0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Game Player Matchmaking(GPM)でチャット作った!①

Last updated at Posted at 2021-11-02

チャットを作るまでの経緯

ふと社内で Tencent Cloud を試す機会がやってきました・・!
Tencent Cloud では色々なプロダクトが提供されていますが、その中でも筆者が目を引いたのはGame Player Matchmaking(以下GPM)」!!!。

ゲーム運営に携わる中で、マルチ対戦のようなネットワーク機能にあまり触れなかったことが目を引いた要因です。
せっかく試す機会が訪れたので、「GPMを使って何か形のあるモノを作ろう!チャットなら出来るかも!」という想いでチャットを作ることにしました。

本記事では2回に分けてチャットの制作と使用したサービスについて紹介し、ゲームへ適応することを考えていこうと思います!
  • 1回目(本記事)ではGPMの主要な機能と最小構成で作るチャットについて紹介します。
  • 2回目「Game Player Matchmaking(GPM)でチャット作った!②」では Game Server Elastic-scaling(GSE)という別のサービスとGPMを連携したチャットの完成系と、ゲームサービスへの適応について紹介します。

GPMとは?

####「ゲームプレイヤー同士のマッチング処理をサービス化」
したものと言えます。
例えば以下に挙げた「マッチング処理」の部分を担当してくれます。

  • 格闘ゲームのネットワーク対戦において対戦相手を決める「マッチング処理」
  • スマートフォンアプリのGvGで同ランク帯のギルドを対戦させる「マッチング処理」

仮にプログラムで実装したなら、複雑なソースコードを書くことになるはずです。
しかしGPMでは「ゲームプレイヤーの情報を渡す」「ルールをjsonで定義する」だけで簡単にマッチングを実現してくれます!

今回作ったチャットの全体像

詳細については2回の記事に分けて紹介します。
GPMを利用して作成したチャットの全体構成は下図となります。
全体像.jpg

GPMで出来ること

今回利用するGPMについて初めて耳にする方もいると思うので、もう少し詳細に紹介します!

ルール

GPMにおけるルールとは「プレイヤー同士がどのような条件でマッチングされるか」を json で定義したものです。
GPMはこのルール定義に従ってマッチングを行います。

GPMが提供するデフォルト(最低限必要な)ルールは以下となります。

default_rule.json
{
	"version": "v1.0",
	"teams": [
		{
			"name": "red",
			"minPlayers": 1,
			"maxPlayers": 1
		},
		{
			"name": "white",
			"minPlayers": 1,
			"maxPlayers": 1
		}
	]
}

まだピンと来ない方もいると思うので、筆者なりの解釈を表にしました。

キー名 意味
version バージョン情報。執筆時点でv1.0以外は指定不可
teams マッチングの対象となるグループのような概念。
name チーム名
minPlayers チームを構成する最低プレイヤー数
maxPlayers チームを構成する最大プレイヤー数
「teams」というキーで定義する配列がマッチングの対象となる単位です。
デフォルトルールは「redチーム(構成プレイヤー1名)vs whiteチーム(構成プレイヤー数1名)」でマッチングを行います。
例えば格闘ゲームのネットワーク対戦は1プレイヤー vs 1プレイヤーなので上記のルールに当てはまります。

もう少し例を挙げます。

以下は「guildA(構成プレイヤー1~20名)vs guildB(構成プレイヤー1~20名)」のマッチングを行うルールです。
「guildA」「guildB」はゲームにおけるギルドを表します。
1プレイヤーでもギルドが成立する状況下では、ギルドの人数差があってもGPMはマッチングを行います。

e.g.)20vs20
{
	"version": "v1.0",
	"teams": [
		{
			"name": "guildA",
			"minPlayers": 1,
			"maxPlayers": 20
		},
		{
			"name": "guildB",
			"minPlayers": 1,
			"maxPlayers": 20
		}
	]
}

以下は「50人のプレイヤーが参加するバトルロワイヤル」のマッチングを想定したルールです。
チームは50人の単独プレイヤーで構成され、自分以外は敵となります。

e.g.)1vs1vs...vs1
{
	"version": "v1.0",
	"teams": [
		{
			"name": "player1",
			"minPlayers": 1,
			"maxPlayers": 1
		},
		{
			"name": "player2",
			"minPlayers": 1,
			"maxPlayers": 1
		},
		(中略)
		{
			"name": "player50",
			"minPlayers": 1,
			"maxPlayers": 1
		}
	]
}

このようにルールはゲームの要件に合わせてjsonで定義するだけで柔軟に設定可能です。
その他の特徴として「teams」配列で指定した全てのチームが揃って初めてマッチングが成立します。
バトルロワイヤルの例では50名のプレイヤーが揃わない限りマッチングは行われません。

マッチ

GPMのマッチはマッチングの設定を行います。
作成したルールの関連付けや、マッチングまでのタイムアウトなどの設定を行うことでマッチングが可能となります。

以下は Tencent Cloud のコンソールでマッチを作成する際のスクリーンショットです。
スクリーンショット 2021-10-14 235624.png

ルールと同様に筆者の解釈を表にしました。

キー名 意味
Name マッチの名前
Description マッチの説明
Associate Rule 関連付けるルール。このルールに則ってマッチングを行う。
Timeout マッチングの待ち時間
Notification Address イベント通知先のURL

「Associate Rule」は作成したルールを関連付けます。
関連付けるとマッチはそのルールに則ってマッチングを行います。

「Timeout」はマッチングを開始してから何秒間待つかの設定です。
仮に30で設定すると、マッチングを開始して30秒以内に相手が見つからなければタイムアウトとなります。

「Notification Address」は開始したマッチングの状態を指定したURLに送信する設定です。
これはGPMのイベント通知機能で、GPMは成立やタイムアウトなどマッチングの状態が変化する度にイベント通知を行います。

以下はイベント通知のサンプルです。
マッチングが成立すると MatchTickets.Status の部分がCOMPLETEDに変化します。

event_notification_sample
{ Time: '2021-10-15T13:54:22Z',
  Account: '200017500577',
  Region: 'ap-shanghai',
  Source: 'TencentCloud.GPM',
  Type: 'MatchmakingSearching',
  Id: '7lh3woX',
  MatchTickets:
   [ { Id: 'ekuO17j',
       MatchCode: 'match-xxxxxxxx',
       MatchResult: '',
       MatchType: 'GSE',
       Players: [Array],
       Status: 'SEARCHING',
       StatusMessage: '',
       StatusReason: '',
       StartTime: '2021-10-15T13:54:22Z',
       EndTime: '',
       TicketType: 0,
       GameServerSessionId: '' } ],
  CustomEventPushData: '',
  MatchTokens:
   [ 'cad4e2e6-2dbe-11ec-96f9-525400db36db',
     '17e05e67-2dbe-11ec-a00b-52540074594e' ] }

今回はこのイベント通知の仕組みを使ってマッチングの状態を取得し、チャットを作成します。

Matchmaking Process APIs

ルールで条件を設定し、マッチで試合の設定が終わるとマッチングのリクエストが可能です。
GPMでは「Matchmaking Process APIs」と呼ばれるマッチングの操作を行うAPIが用意されているのでこれを利用します。
※また Tencent Cloud では「API Explorer」(コンソール上でパラメータを指定し、リクエストを検証出来るサービス)が提供されているので、手軽にAPIを試すことが可能です。

各種APIについて筆者の解釈を表にしました。

API 機能
StartMatching マッチングを開始する
StartMatchingBackfill 「Game Player Matchmaking(GPM)でチャット作った!②」で紹介します
ModifyToken マッチングの認証を行うトークンを発行する
DescribeToken 最後に発行されたマッチング認証トークンを取得する
DescribeMatchingProgress マッチチケットIDで指定したマッチングのステータスを取得する
CancelMatching マッチングをキャンセルする

「StartMatching」はその名の通りマッチングをリクエストします。
マッチングのリクエストは全てチケットという概念で管理され、チケットが発行されるとそのIDが付与されます。

「CancelMatching」は「StartMatching」で開始したマッチングを途中でキャンセルします。

「ModifyToken」はイベント通知されたマッチング結果を検証するためのトークンを発行します。
トークンは上述したイベント通知の MatchTokens というキーに付与されます。
トークンを使用することで、GPMを偽装したイベント通知の検証が可能となり、セキュリティ対策として必須です。

「DescribeToken」は ModifyToken で最後に発行されたトークンを取得します。
DescribeToken から取得したトークンと、イベント通知の MatchTokens の値を検証することマッチング情報の偽装を排除します。

以上がGPMの主な機能です!それではさっそくチャットを構成していきます!

開発環境

始めに作成したチャットの開発環境から紹介します。

■クライアント(Unity)

必須なツールは各サーバとの通信で使用している「WebSocketSharp」のみです。
その他のライブラリはお好みで!

ツール バージョン
Unity 2020.3.19f1
UniRx 7.1.0
extenject 9.2.0
WebSocketSharp WebSocketSharpのリポジトリから手動ビルド

■マッチングサーバ(node.js)

クライアントから要求されたマッチングの管理を担当するサーバ。
GPMへマッチングをリクエストするため「tencentcloud-sdk-nodejs」を入れています。

ツール バージョン
TencentOS Server(CVM) 3.1
git(TencentOS-AppStream) 2.27
nodejs 10.21.0
express 4.17.1
express-ws 5.0.2
node-cron 3.0.0
tencentcloud-sdk-nodejs 4.0.212

■チャットサーバ(Unity)

マッチング成立後、マッチングされたプレイヤー間でチャットをするためのサーバです。
クライアントと同じ「WebSocketSharp」を使用してチャットメッセージをやり取りします。

ツール バージョン
Unity 2020.3.19f1
WebSocketSharp WebSocketSharpのリポジトリから手動ビルド
grpc_unity_package 2.42.0
grpc-protoc_windows_x64 1.42.0

最小構成から作るチャット

チャットを作る際、一番最初に考えた最小構成から紹介します!
(※実際に最初はこのような構成でしたw)

以下が最初に考えたサーバ1台 + GPMで完結する構成です。
最低限のチャット.jpg

実装全てを紹介するのは難しいので各通信部分について紹介します。

①WebSocket部分

クライアント⇔サーバ間をWebSocketで双方向通信します。
主な用途はチャットのブロードキャストとマッチングのリクエスト制御です。

クライアントはWebSocketSharp、サーバはexpress-wsを使ってコネクションを張ります。
コネクションを張った後は json 形式のメッセージでやり取ります。

request_json
{
	"id": "595c644a-7519-4c5f-8205-8795bea7601d",
	"name": "名無しさん",
	"action": "MATCH",
	"message": ""			
}

各パラメータはUnityで以下のように実装しています。

パラメータ 内容
id プレイヤーを特定するID。
C#のGuid.NewGuid()で生成してPlayerPrefsに保持。
name チャットに表示するユーザー名(InputFieldで入力)
action サーバに要求するアクション文字列。
マッチングなら"MATCH"、チャットなら"CHAT"のように指定
message チャットのメッセージ(マッチングの時はempty)

サーバは受け取ったactionに応じて処理を切り替えます。

process_request
    /**
     * メッセージ受信処理
     * @param webSocket クライアントのWebSocket
     * @param message メッセージ
     */
    onMessage(webSocket, message)
    {
        // 確認用ログ
        console.log(message);

        // パース
        const request = JSON.parse(message);
        switch (request.action)
        {
            case "MATCH":
                this.onRequestMatch(webSocket, request);
                break;

            // チャットやキャンセル処理など
        }
    }

②マッチング要求部分

サーバからGPMへマッチングをリクエストする部分です。
「secretId」「secretKey」 には Tencent Cloud の Cloud Access Management で発行して設定します。
「region」 はGPMのルールやマッチを作成したリージョンを指定します。
※GPMへリクエストを送信するサンプルコードは「API Explorer」のCodeGeneratingで言語毎に確認可能です。

matching_request
    /**
     * クライアントがマッチングをリクエストした時の処理
     * @param webSocket クライアントのWebSocket
     * @param request クライアントからのリクエスト
     */
    onRequestMatch(webSocket, request) {

        // リクエストパラメータ作成
        const params = {
            "MatchCode": GpmSettings.getMatchCode(),
            "Players": [
                {
                    "Id": request.id,
                    "Name": request.name,
                }
            ]
        };

        // GPMにリクエスト
        GpmRepository.startMatching(params).then(data => {

            // チケットIDを関連付ける
            webSocket.MatchTicketId = data.MatchTicketId;
            clientSockets.push(webSocket);

            // 確認用ログ
            console.log(data);
        }, err => {

            // エラーログ
            console.error(err);

            // リクエスト失敗
            const response = JSON.stringify({
                action: "ERROR",
                message: err
            });
            webSocket.send(response);
        });

③マッチング結果(イベント通知)部分

②でマッチングのリクエストを行った後、GPMはマッチで設定したURLにイベント通知を送ります。
イベント通知を受けとれるようにサーバを設定します。

イベント通知を受け取ったら DescribeToken API で最後に発行されたトークンを取得し、
偽装されたマッチング情報を排除します。

最後にクライアントへマッチングの状態をレスポンスします。
レスポンスの形式はリクエスト時と同様の json 形式です。

マッチングが完了状態になった時に、クライアントはチャット画面に遷移させます。

event_notification
// request body の json を取得出来るようにする
app.use(express.json());

// GPMのイベント通知を受ける
app.post('/', (req, res) => {

    console.log(req.body);

    // GPMに最後に発行したトークンを取得
    const params = {"MatchCode": GpmSettings.matchCode};
    gpmClient.DescribeToken(params).then((data) => {

        console.log(data);

        // トークンチェック
        const authorized = req.body.MatchTokens.some(e => e == data.MatchToken);
        req.body.MatchTickets.forEach(matchTicket => {

            let message = "";            
            const ws = clientSockets.find(e => e.MatchTicketId == matchTicket.Id);

            if (authorized)
            {
                switch (matchTicket.Status) {
                    // マッチング完了
                    case "COMPLETED":
                       break;

                    case "SEARCHING":
                        message = "マッチング中です。しばらくおまちください。";
                        break;

                    case "CANCELLED":
                        message = "キャンセルしました。";
                        break;

                    case "TIMEDOUT":
                        message = "タイムアウトしました。"
                        break;
                }

                // メッセージを送る
                ws.send(JSON.stringify({action: matchTicket.Status, message: message}));
            }
            else
            {
                // エラーにする
                ws.send(JSON.stringify({action: "ERROR", message: "トークンの検証が出来ませんでした。"}));
            }
        });
    });
})

これでGPMのマッチングを利用した最小構成のチャットは出来るようになります。

ルールを設定する

GPMのデフォルトルールは1チーム(プレイヤー数1)vs1チーム(プレイヤー数1)だけでそれ以外のルールはありません。
実際のゲームではもっと複雑なルールを使用したマッチングが必要になります。
そこでルールの追加を考えてみます。

Distance Ruleの追加

Distance Rule は名の通り特定の値から一定の距離にある範囲内をマッチング対象とするルールです。
これを記事の序盤で紹介したデフォルトルールに追加してみます。
Tencent Cloud のGPMコンソールから Rule を選択し、作成画面で「Add Distance Rule」を選択します。
スクリーンショット 2021-10-15 152542.png

以下の項目が追加されます。
(※分かりやすくするため一部値を変更しています)

追加された項目
	"playerAttributes": [
		{
			"name": "age",
			"type": "number"
		}
	],
	"rules": [
		{
			"name": "ageRule",
			"type": "distanceRule",
			"description": "This is a distanceRule example, the avg attributes1 of each team must equals to the avg attributes1 of the same game session",
			"measurements": [
				"flatten(teams[*].players.playerAttributes[age])"
			],
			"referenceValue": "avg(flatten(teams[*].players.playerAttributes[age]))",
			"maxDistance": 10
		}
	]

各パラメータについて筆者の解釈をまとめました。

パラメータ 子パラメータ 内容
playerAttributes プレイヤーの属性。マッチングのルールに必要なプレイヤーの属性を配列で複数指定する。
name 属性の名前
type 属性のタイプ
rules ルール。相手を探す時の条件を配列で複数指定。
name ルールの名前
type ルールの種類
description ルールの説明
measurements distanceルールの場合、referenceValueと比較する値
referenceValue distanceルールの場合、measurementsと比較する値
maxDistance measurementsとreferenceValueの差の最大値制約

「playerAttributes」はチームに所属するプレイヤーの属性(実際はデータ)を設定します。
例えば格闘ゲームのネットワーク対戦を考えたとき、なるべく同レベルのプレイヤー同士をマッチングさせたいと考えますよね。
その際「勝利数」や「ランク」などのプレイヤーデータを基に近いプレイヤーをマッチングさせると思います。
この「勝利数」や「ランク」が属性です。
属性は必要に応じて複数設定可能です。

「rules」は属性を使ってマッチングの条件を設定します。
Distance Rule の場合は、「refereneceValue」が基準値、「measurements」が比較対象値、「maxDistance」が範囲を表します。

例えば「年齢」という属性でルールの作成を考えてみます。
「30歳を基準にて+-10歳」(=なるべく年齢が近いプレイヤー)でマッチングしたい場合は以下のように設定します。
「flatten」は引数に指定した値を1つのリストにまとめる関数です。

sample_distance_rule
    "playerAttributes": [
        {
            "name": "age",
            "type": "number"
        }
    ],
    "rules": [
        {
            "name": "ageRule",
            "type": "distanceRule",
            "description": "This is a distanceRule example, the avg attributes1 of each team must equals to the avg attributes1 of the same game session",
            "measurements": [
                "flatten(teams[*].players.playerAttributes[age])"
            ],
            "referenceValue": 30,
            "maxDistance": 10
        }
    ]

Comparison Rule の追加

Comparision Rule も名の通り比較を行います。

sample_comparison_rule
	"playerAttributes": [
		{
			"name": "gender",
			"type": "number"
		}
	],
	"rules": [
		{
			"name": "genderRule",
			"type": "comparisonRule",
			"description": "This is a complarisonRule example, players with the same attributes1 can be matched to a same game session",
			"measurements": [
				"flatten(teams[*].players.playerAttributes[gender])"
			],
			"operation": "="
		}
	]

Distance Rule との大きな違いは「operation」です。
operation は measurements に指定されたそれぞれの値を operation で指定した符号で比較します。
サンプルでは各プレイヤーの gender(性別)が等しい場合にのみマッチングが成立します。

チャットで女子会やる場合は上述のように設定し、お見合いをする場合は operation に "!=" を設定することで異性とマッチングが出来るようになります。

このようにGPMでは、コンソール上のスクリプトを編集するだけで簡単にルールを追加することが出来ます!

ルールをチャットに反映する

作成した「性別」「年齢」に加えて「年収」という3つのルールをチャットに反映してみます。

Unityで各属性を入力するInputFieldを作成します。
スクリーンショット 2021-10-16 003432.png

入力した情報をGPMに伝えるため、json に playerAttributes という項目で「年齢」「性別」「年収」のパラメータを追加します。

playerAttributes
{
	"id": "595c644a-7519-4c5f-8205-8795bea7601d",
	"name": "名無しさん",
	"action": "MATCH",
	"message": "",
	"playerAttributes": {
		"gender": 0,
		"age": 10,
		"annualIncome": 0
	}
}

サーバではクライアントから受け取ったjsonを使ってGPMへリクエストを送信します。

サーバ側でplayerAttributesを添えてリクエスト
    onRequestMatch(webSocket, request) {

        // リクエストパラメータ作成
        const params = {
            "MatchCode": GpmSettings.getMatchCode(),
            "Players": [
                {
                    "Id": request.id,
                    "Name": request.name,
                    "MatchAttributes": [
                        {
                            "Name": "gender",
                            "Type": 0,
                            "NumberValue": request.playerAttributes.gender
                        },
                        {
                            "Name": "age",
                            "Type": 0,
                            "NumberValue": request.playerAttributes.age
                        },
                        {
                            "Name": "annualIncome",
                            "Type": 0,
                            "NumberValue": request.playerAttributes.annualIncome
                        }
                    ]
                }
            ]
        };

        // リクエスト処理へ

これでGPMはリクエストに追加した属性を参照し、ルールに合ったプレイヤー同士のマッチングが可能になります。

チャットの課題

ここまでの実装でお見合い or 女子会チャットくらいにはなりましたw
しかしチャットとしての機能にはまだまだ課題があります。

課題

・チャットルームがない

最小限の構成ではルーム概念(変数なりクラスなり)を実装しない限り、
マッチングが成立したとしても接続した全てのクライアントにメッセージを送ってしまいます。

・途中参加させたい

2名のプレイヤーがマッチングするルールを作成したので、後からプレイヤーが参加出来ません。
チャットでは途中からどんどん人が参加し盛り上がっていくのがセオリーだと思います。

「Game Player Matchmaking(GPM)でチャット作った!②」で上記課題を解決していきます!

終わりに

今回 Tencent Cloud に触れることで初めてGPMのようなサービスがあることを知りました。
新しいサービスを知り、そして実際に使用して理解するということは、今後もゲーム開発に携わる上で非常に大きなアドバンテージだと思います。
また自身のキャリアにも繋がるので読者の皆様も是非お試しください!

株式会社マイネットでは一緒に働く仲間を募集しています!
弊社では様々なゲームタイトルをより長く、安定して運営していくために、インフラ最適化にも積極的に取り組んでいます。興味のある方、ご応募お待ちしております!
https://mynet.co.jp/recruitment/

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?