GS2-Matchmaking

マッチメイキング機能

プレイヤーを条件に基づいてグルーピングする機能です。 対戦相手を見つけるために利用することができます。

スクリプトトリガー

ネームスペースに createGatheringTriggerScriptIdcompleteMatchmakingTriggerScriptIdchangeRatingScript を設定すると、ギャザリング作成完了時・マッチング完了時・レーティング変動時にカスタムスクリプトを実行できます。

設定できる主なイベントトリガーとスクリプト設定名は以下の通りです。

  • createGatheringTriggerScriptId: ギャザリング作成完了時
  • completeMatchmakingTriggerScriptId: マッチング完了時
  • changeRatingScript: レーティング変動時

プッシュ通知

設定できる主なプッシュ通知と設定名は以下の通りです。

  • joinNotification: マッチング参加時に通知
  • leaveNotification: マッチング離脱時に通知
  • completeNotification: マッチング完了時に通知
  • changeRatingNotification: レーティング変動時に通知

いずれも GS2-Gateway 経由で通知でき、オフライン端末へのモバイルプッシュ転送も可能です。

マスターデータ運用

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

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

  • RatingModel: レーティング計算設定
  • SeasonModel: シーズン期間設定

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

追加設定

enableDisconnectDetection を有効化するとマッチメイキング中の切断検知を行い、disconnectDetectionTimeoutSeconds でタイムアウトを調整できます。また enableCollaborateSeasonRating を設定するとマッチメイキング完了時に GS2-SeasonRating と連携し、結果を受け付けるセッションを生成できます。

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

GS2-Matchmaking では以下のトランザクションアクションを提供しています。

  • 検証アクション: 永続ギャザリングへの参加状況の検証

「永続ギャザリングへの参加状況の検証」を検証アクションとして利用することで、特定のシーズンギャザリング(チームやクラスター)に所属しているプレイヤーのみが受け取れる報酬の設定などが可能になります。これにより、シーズンごとのグループ対抗イベントや、特定のコミュニティ限定のインセンティブ付与といった施策をトランザクション内で安全に制御できます。

ユースケース別設計ガイド

パーティトークンを使ったマッチメイキング

マッチメイキング完了後に1名が離脱し補充したい場合や、事前にパーティを編成してからマッチメイキングを行いたい場合は「パーティトークン」を使用します。

パーティメンバーはそれぞれプレイヤートークン(有効期限3分)を発行し、パーティの代表者に送信します。代表者はパーティトークン発行APIに送信することでパーティトークン(有効期限10分)を入手し、ギャザリング作成またはギャザリング検索を呼び出します。これにより、パーティメンバー全員が参加した状態のギャザリングを作成または参加できます。

パーティ同士のマッチメイキング

たとえば4 vs 4 の対戦で事前に編成したパーティを分散させたくない場合、まず4人のマッチメイキングを実行してパーティ単位のギャザリングを作成し、その後各パーティの代表者が定員2人のマッチメイキングを行うことでパーティ同士のマッチメイキングを実現できます。

マッチメイキング中のプレイヤー間の通信

マッチメイキング完了前にプレイヤー間でのやりとりが必要な場合、ギャザリング作成時に以下の2つの方法を使用できます。

  • GS2-Chat と連携したチャットルーム作成: メタデータの低頻度(秒間3回以内)交換に適しています
  • GS2-Realtime と連携したゲームサーバの起動: 高頻度の通信や、人数が不足した状態でゲームを遊ばせたい場合に適しています

マッチメイキング完了後の処理

マッチメイキングが完了したときに以下の方法でプレイヤーに通知できます。

  • GS2-Gateway を使用したゲーム内プッシュ通知
  • GS2-JobQueue によるジョブ登録

また、GS2-Script を使用して任意のスクリプトを実行することもできます。GS2-Realtime を使用する場合は、マッチメイキング完了後に実行される GS2-Script で GS2-Realtime のギャザリングを作成し、IPアドレス・ポート情報をプッシュ通知またはジョブキューを使って通知することでゲームサーバーに誘導できます。

ブラックリストによるマッチング除外

ギャザリング作成時・検索時にユーザーIDのブラックリストを指定できます。検索時に指定したブラックリストは、参加したギャザリングのブラックリストに加えられます。自身がブラックリストに入っている場合はマッチメイキングの対象から除外されます。

スタンダードマッチメイキング

ギャザリング

ギャザリングはマッチメイカーによってグルーピングされたプレイヤーの集合です。 ギャザリングのライフサイクルは最初プレイヤーによって明示的に作成されます。 他のプレイヤーがマッチメイキングを実行し、条件に見合う場合はギャザリングに参加してきます。

ギャザリング作成時に指定した人数のプレイヤーが集まると、マッチメイキング結果をプレイヤーに通知して GS2-Matchmaking 上からはギャザリングは削除されます。

属性

プレイヤーは最大5個の属性値を持つことができます。 ギャザリングを作成する際には、募集するプレイヤーの各属性値の範囲を設定してギャザリングを作成します。

例えば、以下の属性値をもつプレイヤーがいたとしましょう。

属性名 属性値
GameMode 1
Stage 10
Level 5

GameMode というのは、ゲーム内で全世界のプレイヤーをマッチメイキング対象とする(1)か、各リージョンごとにマッチメイキングする(2=JP, 3=US, 4=EU)か 希望するゲームモードとしましょう。 Stage には対戦に使用したいと思っているゲーム内のステージの種類を示しているとします。 最後に、Level はプレイヤーの技術レベルです。

このプレイヤーが対戦相手を見つけるためにギャザリングを作成する場合、ギャザリングにどのような条件設定をするのがよさそうか考えてみましょう。

属性名 属性値(最小) 属性値(最大)
GameMode 1 1
Stage 10 10
Level 2 8

上記のような範囲設定がよいでしょう。 GameMode や Stage は完全一致していないとプレイヤーの望む条件で遊ばせることができません。 Level はプレイヤーのレベルの前後3のプレイヤーを対象としましょう。

これでギャザリングを作成することで、条件にみあうプレイヤーがマッチメイキングリクエストを出すとギャザリングに参加してきます。

ロール

ギャザリングを作成するときには募集人数を設定する必要があります。 募集人数を設定する際に、ロールごとに募集人数を設定できます。 特にゲーム内に役割がない場合は、1つだけ default という名前のロールを設定し、そのロールの募集人数に集めたい人数を指定します。

ロールの指定が必要になるケースは、ゲーム内に役割があるようなゲームです。 たとえば、MMORPGでは、プレイヤーの職業によって タンク・ヒーラー・DPS といったゲーム内の役割が存在します。 そして、マッチメイキングを行った結果 ヒーラー4人が集まってもコンテンツは攻略できません。 各ロールがバランスよくマッチメイキングされる必要があります。

このような仕様において、以下のようなロールと募集人数を設定します。

ロール名 募集人数
タンク 1
ヒーラー 1
DPS 2

そして、プレイヤーはマッチメイキングリクエストを出す時に、属性値だけでなく自分のロールも設定します。 こうすることで、ロールごとに募集人数の枠管理が行われ、マッチメイキングが実行されます。

ロールのエイリアス

さらに複雑な条件設定を元にロールベースマッチメイキングを行う例を紹介します。 DPSには近距離DPSと遠距離DPSの2種類があったとします。 基本的なマッチメイキング条件は先程解説した通りでいいのですが、ギャザリング作成者のこだわりで 近距離DPSと遠距離DPSを一人ずつ入ってきて欲しいとしましょう。

その場合、条件設定は以下になります。

ロール名 募集人数
タンク 1
ヒーラー 1
近距離DPS 1
遠距離DPS 1

そして、プレイヤーはマッチメイキングをする時に「DPS」ではなく「近距離DPS」もしくは「遠距離DPS」を指定します。 しかし、このままでは「DPS が2人集まってくれれば近距離でも遠距離でもどちらでもいいよ」という人とマッチメイキングできなくなってしまいます。

そこで使用できる機能がロールのエイリアスです。 プレイヤーがロールに「近距離DPS」もしくは「遠距離DPS」のいずれかを設定するという部分は変わりませんが、ギャザリングを作成する際の条件設定が変わります。

ロール名 エイリアス 募集人数
タンク [] 1
ヒーラー [] 1
DPS [近距離DPS, 遠距離DPS] 2

これで、DPS の募集枠にはロールに「近距離DPS」や「遠距離DPS」をしているプレイヤーも入れるようになります。

レーティング計算

プレイヤーの強さを表現するレーティング値の計算機能の用意があります。 レートの値は初期値が1500で、ゲームの結果によって値が上下します。

レート値の差が大きいプレイヤー同士でプレイし、レートの値が大きいプレイヤーが負けると レート値の高い負けたプレイヤーのレート値は大幅に下落し、レート値の低い勝ったプレイヤーのレート値は大幅に上昇します。 この特性によって、プレイヤーの実力を反映したレート値を計算します。

投票

レート値を変更するには、投票処理が必要となります。 対戦が終わり、順位が確定すると、各プレイヤーはゲームの結果をサーバーに送信します。 サーバーはギャザリングごとに投票を受け付け、全てのプレイヤーが投票を終えるか、最初のプレイヤーが投票して5分が経過すると投票を締め切ります。

サーバーでは投票内容で多数決を取り、最終的な結果を確定します。 このプロセスによって、嘘の投票を行う悪意のあるプレイヤーがいたとしても結果を反映しない力が働きます。 多数決によって確定した順位に基づき、新しいレート値が計算され反映されます。

投票結果の分断

サーバーが多数決を取ろうとした時に、結果が同数で最終的な結果を確定できない場合はレートの計算が行われません。 そのため、1vs1 のゲームでは正しいレート値を求めるのは困難です。 この問題を解決するためには、裏で対戦には直接関わらない3人目のプレイヤーをマッチメイキングし、そのプレイヤーに第三者視点で投票してもらうといった工夫が必要となります。

ギャザリングの有効期限

ギャザリングは原則として一度作成すると、マッチメイキングが成立するか全てのプレイヤーがギャザリングから抜けるまで削除されません。 特にプレイヤーが少ないゲームでマッチメイキング中にプレイヤーがキャンセルAPIを呼び出さずにゲームを終了した場合 他のプレイヤーがマッチメイキングリクエストを出して、マッチメイキングが成立してもすでにギャザリングを作成したプレイヤーがいない という状況が発生し得ます。

このような問題を簡単に解決する手段として、ギャザリング作成時にギャザリングの有効期間を設定できます。 指定した時刻、または作成時刻からの経過時間を指定することで、その時刻になるとギャザリングは削除されます。 その際、すでにギャザリングに参加しているプレイヤーは自動的に退出処理が行われます。

実装例

ギャザリングを作成

    var result = await gs2.Matchmaking.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).CreateGatheringAsync(
        player: new Gs2.Unity.Gs2Matchmaking.Model.EzPlayer {
             Attributes = new [] {
                new Gs2.Unity.Gs2Matchmaking.Model.EzAttribute {
                    Name = "stage",
                    Value = 1,
                },
                new Gs2.Unity.Gs2Matchmaking.Model.EzAttribute {
                    Name = "level",
                    Value = 10,
                },
            },
        },
        attributeRanges: new [] {
            new Gs2.Unity.Gs2Matchmaking.Model.EzAttributeRange {
                Name = "stage",
                Min = 1,
                Max = 1,
            },
            new Gs2.Unity.Gs2Matchmaking.Model.EzAttributeRange {
                Name = "level",
                Min = 0,
                Max = 10,
            },
        },
        capacityOfRoles: new [] {
            new Gs2.Unity.Gs2Matchmaking.Model.EzCapacityOfRole {
                RoleName = "default",
                Capacity = 4,
            },
        },
    );
    var item = await result.ModelAsync();
    const auto Future = Gs2->Matchmaking->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->CreateGathering(
        MakeShared<Gs2::Matchmaking::Model::FPlayer>()
            ->WithAttributes([]
            {
                const auto v = MakeShared<TArray<TSharedPtr<Gs2::Matchmaking::Model::FAttribute>>>();
                v->Add(MakeShared<Gs2::Matchmaking::Model::FAttribute>()
                    ->WithName(TOptional<FString>("stage"))
                    ->WithValue(TOptional<int32>(1)));
                v->Add(MakeShared<Gs2::Matchmaking::Model::FAttribute>()
                    ->WithName(TOptional<FString>("level"))
                    ->WithValue(TOptional<int32>(10)));
                return v;
            }()),
        []
        {
            const auto v = MakeShared<TArray<TSharedPtr<Gs2::Matchmaking::Model::FAttributeRange>>>();
            v->Add(MakeShared<Gs2::Matchmaking::Model::FAttributeRange>()
                ->WithName(TOptional<FString>("stage"))
                ->WithMin(TOptional<int32>(1))
                ->WithMax(TOptional<int32>(1)));
            v->Add(MakeShared<Gs2::Matchmaking::Model::FAttributeRange>()
                ->WithName(TOptional<FString>("level"))
                ->WithMin(TOptional<int32>(0))
                ->WithMax(TOptional<int32>(10)));
            return v;
        }(), // attributeRanges
        []
        {
            const auto v = MakeShared<TArray<TSharedPtr<Gs2::Matchmaking::Model::FCapacityOfRole>>>();
            v->Add(MakeShared<Gs2::Matchmaking::Model::FCapacityOfRole>()
                ->WithRoleName(TOptional<FString>("default"))
                ->WithCapacity(TOptional<int32>(4)));
            return v;
        }(), // capacityOfRoles
        nullptr, // allowUserIds
        nullptr, // expiresAt
        nullptr // expiresAtTimeSpan
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

マッチメイキング処理を実行

    var items = await gs2.Matchmaking.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).DoMatchmakingAsync(
        player: new Gs2.Unity.Gs2Matchmaking.Model.EzPlayer {
             Attributes = new [] {
                new Gs2.Unity.Gs2Matchmaking.Model.EzAttribute {
                    Name = "stage",
                    Value = 1,
                },
                new Gs2.Unity.Gs2Matchmaking.Model.EzAttribute {
                    Name = "level",
                    Value = 10,
                },
            },
        },
    ).ToListAsync();
    const auto It = Gs2->Matchmaking->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->DoMatchmaking( // player
    );
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }

ギャザリングから退出

    var result = await gs2.Matchmaking.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Gathering(
        gatheringName: "gathering-0001"
    ).CancelMatchmakingAsync(
    );
    const auto Future = Gs2->Matchmaking->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Gathering(
        "gathering-0001" // gatheringName
    )->CancelMatchmaking(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

レート値を取得

    var item = await gs2.Matchmaking.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Rating(
        ratingName: "rating-0001"
    ).ModelAsync();
    const auto Domain = Gs2->Matchmaking->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Rating(
        "rating-0001" // ratingName
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
    const auto Item = Future->GetTask().Result();

投票用紙を取得

    var item = await gs2.Matchmaking.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Ballot(
        ratingName: "rating-0001",
        gatheringName: "gathering-0001",
        numberOfPlayer: 4,
        keyId: "grn:gs2:{region}:{yourOwnerId}:key:namespace-0001:key:key-0001"
    ).ModelAsync();

    var body = result.Body;
    var signature = result.Signature;
    const auto Domain = Gs2->Matchmaking->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Ballot(
        "rating-0001", // ratingName
        "gathering-0001", // gatheringName
        4, // numberOfPlayer
        "key-0001" // keyId
    );
    const auto Future = Domain->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
    const auto Item = Future->GetTask().Result();

投票を実行

    var result = await gs2.Matchmaking.Namespace(
        namespaceName: "namespace-0001"
    ).VoteAsync(
        ballotBody: "ballotBody",
        ballotSignature: "ballotSignature",
        gameResults: new [] {
            new Gs2.Unity.Gs2Matchmaking.Model.EzGameResult
            {
                Rank = 1,
                UserId = "user-0001",
            },
            new Gs2.Unity.Gs2Matchmaking.Model.EzGameResult
            {
                Rank = 2,
                UserId = "user-0002",
            },
            new Gs2.Unity.Gs2Matchmaking.Model.EzGameResult
            {
                Rank = 2,
                UserId = "user-0003",
            },
            new Gs2.Unity.Gs2Matchmaking.Model.EzGameResult
            {
                Rank = 3,
                UserId = "user-0004",
            },
        }
    );
    const auto Future = Gs2->Matchmaking->Namespace(
        "namespace-0001" // namespaceName
    )->Vote(
        "ballotBody",
        "ballotSignature",
        []
        {
            const auto v = MakeShared<TArray<TSharedPtr<Gs2::Matchmaking::Model::FGameResult>>>();
            v->Add({'rank': 1, 'userId': 'user-0001'});
            v->Add({'rank': 2, 'userId': 'user-0002'});
            v->Add({'rank': 2, 'userId': 'user-0003'});
            v->Add({'rank': 3, 'userId': 'user-0004'});
            return v;
        }() // gameResults
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

よくある質問

存在するギャザリングをリストアップしてプレイヤーに選択させたい

GS2-Matchmaking ではそのような機能は提供していません。 これには技術的な理由ではなく、ゲーム開発者の一人としてのプレイヤーのゲーム体験を損なわないためのポリシーが理由です。

存在するギャザリングから選択する形であれば、プレイヤーにとって最善のギャザリングに参加する方法を提示できます。 しかし、ギャザリングのリストを取得してから、実際に参加操作を行うまでにユーザー操作による遅延が発生します。 この遅延によって、参加したいギャザリングを選択した時には既にギャザリングが満員になっているようなケースが避けられません。

GS2の開発者は過去にこのようなゲームを多数プレイし、非常に大きなフラストレーションを感じてきました。 GS2-Matchmaking ではプレイヤー自身にギャザリングを選択させなくても、自動的に最適なギャザリングを探し出せるだけの仕組みを提供している自負があります。 そのため、このような機能は提供していません。

ギャザリングにパスワードを設定したい

パスワードを属性値に設定してマッチメイキング条件の一部としてください。

シーズンマッチメイキング

シーズンマッチメイキングは特定の期間永続的なギャザリングを作成します。 リアルタイム対戦のためのマッチメイキングというよりは、特定期間永続化されるクラスターを形成し、GS2-Ranking2 のクラスターランキングを組み合わせて、ギャザリング内でのランキングを実現するのが一般的な用法です。

シーズン

シーズンマッチメイキングを利用するにあたって、GS2-Schedule のイベントを指定することで期間を設定します。 イベントに繰り返し設定がある場合は繰り返すたびに新しいギャザリングが形成されます。

ティアー

シーズンマッチメイキングに GS2-Experience を組み合わせると特定ランク同士でマッチメイキングすることができます。 GS2-Ranking2 のクラスターランキングにはランキング報酬を設定する機能があり、ランキング上位のプレイヤーに経験値を付与することで上位ティアーにあがることができるようにできます。

最大人数

シーズンマッチメイキングでは最大1000人のプレイヤーをマッチメイキングできます。

マッチメイキング条件

シーズンマッチメイキングでは、スタンダードマッチメイキングのような複雑な検索条件を設定することはできません。 検索条件に利用可能なパラメーターは1つだけ、GS2-Experience のランクのみで、値はAPIの引数として取ることはなく、GS2-Matchmaking が内部処理で GS2-Experience から値を取得します。

GS2-Experience との連携機能を利用しない場合は全てのプレイヤーを対象としてマッチメイキングが行われます。

永続ギャザリングからの退出

スタンダードマッチメイキングでは、マッチメイキングされたギャザリングが退出ができました。 しかし、シーズンマッチメイキングでは退出することはできず、すでにマッチメイキングしたことがある状態でマッチメイキングリクエストを出してもすでに参加しているギャザリングが応答されます。

つまり、シーズンの間は他のギャザリングにマッチメイキングし直す手段は原則ありません。 一つだけ例外があり、それは永続ギャザリングを削除した時で、永続ギャザリングを削除することで参加中のプレイヤーは再度マッチメイキングした際に新しいギャザリングにマッチメイキングされます。

マッチメイキング処理の優先度

シーズンマッチメイキングでは、マッチメイキング時に同一ティアーの永続ギャザリングをで規定人数に達していないギャザリングを探して、ギャザリングに参加します。 参加可能なギャザリングが存在しない場合は自動的にギャザリングを作成し、参加処理を行った上でAPIを応答します。

参加可能なギャザリングが複数存在する場合はより参加人数が多いギャザリングが優先されます。

マッチメイキング処理が競合した時の挙動

マッチメイキングの参加処理は一般的に数ミリ秒以下で処理されますが、それでも同時に同じギャザリングが参加処理の候補に選別されることはあり得ます。 このような場合、規定人数より多くのプレイヤーが参加してしまうことを防ぐために GS2-Matchmaking はギャザリングのロックを取った上でプレイヤーの参加処理を行います。 そのため、参加可能なギャザリングがあったとしても、他のプレイヤーの参加処理中のわずかな期間はマッチメイキング対象にはならず他の候補が選択されます。

この仕様によって、1000人を最大収容可能な永続ギャザリングを用意して、同じティアーのプレイヤー1000人を完全に同時にマッチメイキングリクエストを出した時、1000人が参加する1つの永続ギャザリングが出来上がるとは限らず、複数の永続ギャザリングに分かれてマッチメイキングされる可能性があります。 この時、なるべく参加人数の多いギャザリングに優先してプレイヤーを参加させていきますので、一般的に以下のような結果が想定されます。

ギャザリングAの参加人数 ギャザリングBの参加人数 ギャザリングCの参加人数
1000 - -
999 1 -
998 1 1
997 2 1
995 4 1
995 5 -
990 8 2

この結果はあくまで一例であり、異なるパターンも発生し得ます。

詳細なリファレンス