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 ではプレイヤー自身にギャザリングを選択させなくても、自動的に最適なギャザリングを探し出せるだけの仕組みを提供している自負があります。 そのため、このような機能は提供していません。

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

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

詳細なリファレンス