GS2-Matchmaking

マッチメイキング機能

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

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

ギャザリング

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

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

属性

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

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

属性名属性値
GameMode1
Stage10
Level5

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

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

属性名属性値(最小)属性値(最大)
GameMode11
Stage1010
Level28

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

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

ロール

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

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

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

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

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

ロールのエイリアス

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

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

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

そして、プレイヤーはマッチメイキングをする時に「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 item = Domain.Model();

投票用紙を取得

    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 item = Domain.Model();

投票を実行

    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--
9991-
99811
99721
99541
9955-
99082

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

詳細なリファレンス