こんにちは、ゲームソリューション部の入井です。
Amazon GameLiftでは、FlexMatchという対戦型オンラインゲームなどにおけるプレイヤー同士のマッチングをサポートする機能が提供されています。
FlexMatchは、それ単体で複雑なマッチメイキングロジックを構築することができますが、現在マッチングがどのような状況であるかを確認するには専用のアーキテクチャを組む必要があります。今回は、実際にそのアーキテクチャを構築し、マッチングステータスを受け取る流れを書いていきます。
構築手順
今回のアーキテクチャは、以下のAWS公式ブログを元にしています。
FlexMatchマッチング状態を確認するためのサーバレスアプリケーションの実装例 | Amazon Web Services ブログ
上記の記事は、GameLift FlexMatchは実際には利用せず、周辺のアーキテクチャの動作確認までの内容となっています。今回は、この内容をベースに以下の点で変化をつけて検証をしてみました。
- 実際にGameLift FlexMatchを使ってマッチメイキング結果を出力させる
- LambdaのコードはNode.jsランタイム用のものに変更
- 実際にFlexMatchを使用する都合で一部処理の流れも微調整
FlexMatchマッチング状態を確認するためのサーバレスアプリケーションの実装例 | Amazon Web Services ブログより引用
なお、FlexMatchには、Amazon GameLiftのキューと連携してマッチング結果を元に自動的にゲームセッションやプレイヤーセッションの作成・配置を行うWITH_QUEUEモードと、セッション関係の処理は自前で作成する必要があるSTANDALONEモードがあります。今回の記事は、あくまでFlexMatchのマッチング機能のみに焦点を当てるため、STANDALONEモードで構築します。
なお、STANDALONEモードは、以下のような環境でマッチング関係の機能のみを使用するのに有用です。
- 通常のGameLift(Managed Hosting)よりサーバー選択の自由度が高い FleetIQ環境
- ゲームサーバーがAWS以外で稼働している環境
ただし、WITH_QUEUEモードではFlexMatchは無料で利用できますが、STANDALONEモードでは利用量に応じて費用が発生するのでご注意ください。
マッチング結果通知SNSの設定
FlexMatchでは、ゲームクライアントからのマッチング要求を元にマッチメイキング処理を行う中で、マッチングのステータスが変化するごとにその内容をSNSやEventBridgeを使って外部へ通知することができます。
今回は、参考記事と同様にSNSにて通知を行うことにします。名前はFlexMatchTopic
で、スタンダードトピックとして作成します。
SNSのアクセスポリシーは以下の通りで、GameLiftからのみパブリッシュを許可するようにしています。
{
"Version": "2008-10-17",
"Id": "__default_policy_ID",
"Statement": [
{
"Sid": "EnableGameliftServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "gamelift.amazonaws.com"
},
"Action": "sns:Publish",
"Resource": "arn:aws:sns:ap-northeast-1:[AWSアカウントID]:FlexMatchTopic"
}
]
}
DynamoDBテーブルの作成
続いて、FlexMatchのマッチングステータス情報を保存するためにDynamoDBテーブルを作成します。
テーブル名はFlexMatchResults
、パーティションキーはticketId
で設定しました。
FlexMatchでは、各クライアントからのマッチング要求をチケットという単位で管理します。パーティションキーをticketId
とすることで、任意のチケットの最新ステータスを常に参照できるようにしています。
マッチングステータスを保存するLambda関数の作成
ここまで作成したSNSトピックとDynamoDBテーブルを利用し、FlexMatchから届いたマッチングステータスをDynamoDBへ保存するFlexMatchEventHandler
というLambda関数を作成します。この関数は、SNSからの通知をトリガーに実行します。
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
BatchWriteCommand,
DynamoDBDocumentClient,
} from "@aws-sdk/lib-dynamodb";
const tableName = process.env.MATCHING_TABLE;
export const handler = async function (event, context) {
const dynamoDbClient = new DynamoDBClient({});
const dynamoDbDocClient = DynamoDBDocumentClient.from(dynamoDbClient);
const message = JSON.parse(event['Records'][0]['Sns']['Message']);
const body = message;
console.info('Received event:', JSON.stringify(event, null, 2));
if (!body.detail || !body.detail.type){
console.error('Wrong notification, check whether your notification is from GameLift flexmatch');
return message;
}
switch (body.detail.type) {
// 特定のステータスの場合のみDynamoDBへ書き込み
case "MatchmakingSearching":
case "MatchmakingSucceeded":
case "MatchmakingTimedOut":
case "MatchmakingCancelled":
case "MatchmakingFailed"
const params = {
RequestItems: {}
};
// チケット別に項目を分ける
params.RequestItems[tableName] = [];
body.detail.tickets.forEach(ticket => {
params.RequestItems[tableName].push({
PutRequest: {
Item: {
ticketId: ticket.ticketId,
status: body.detail.type,
players: ticket.players,
time: body.time
}
}
});
});
try {
const command = new BatchWriteCommand(params);
await dynamoDbDocClient.send(command);
} catch (error) {
console.error(error);
}
break;
default:
console.info(body.detail.type);
}
return message;
}
参考記事でPythonで書かれていたのをNode.jsランタイム用に書き換えました。また、基本的な処理の流れは同じですが、参考記事ではMatchMakingSucceeded
ステータスのみDynamoDBへ保存していたのに対し、こちらのコードでは他のステータスも保存するようにしています。これは、どのステータスであってもAPI経由で確認できるようにしたかったからです。
なお、FlexMatchからのイベント通知は、今回コードに記載しているものの他にもプレイヤーによるマッチング承諾機能関係のものがありますが、今回はそれを使用していないので取り扱っていません。他にどのようなイベントがあるかについては、以下のドキュメントを参照してください。
FlexMatchマッチメイキングイベント - アマゾン GameLift
また、このLambda関数用のIAMロールにDynamoDBへのBatchWriteItem
を許可するポリシーを付与しています。
マッチングステータスを確認するLambda関数の作成
ゲームクライアントからのリクエストを元に、指定されたチケットのマッチングステータスを取得するMatchingResultHandler
というLambda関数を作成します。こちらについても、処理の流れは参考記事のPythonコードと同様になっています。
import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
const dynamoDbClient = new DynamoDBClient({});
export const handler = async (event, context) => {
if (!event.queryStringParameters || !event.queryStringParameters.id) {
return serverResponse(400, "Parameter Error");
}
const ticketId = event.queryStringParameters.id;
const params = {
TableName: process.env.MATCHING_TABLE,
Key: marshall({ ticketId: ticketId })
};
try {
const { Item: item } = await dynamoDbClient.send(new GetItemCommand(params));
if (!item) {
return serverResponse(200, 'Match not available');
}
return serverResponse(200, unmarshall(item));
} catch (error) {
console.error(error);
return serverResponse(500, "Internal Server Error");
}
};
const serverResponse = (statusCode, message) => {
return {
statusCode,
headers: {
"Access-Control-Allow-Headers": "Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token",
"Access-Control-Allow-Methods": "DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT",
"Access-Control-Allow-Origin": "*"
},
body: JSON.stringify(message)
};
};
こちらのLambda関数用のIAMロールには、DynamoDBへのGetItemを許可するポリシーを付与しています。
API Gatewayの作成
ゲームクライアントからマッチングステータス確認のリクエストを受けるための窓口としてAPI Gatewayを作成します。設定としては以下の通りで、クエリパラメータ付きのGETリクエストの内容をMatchingResultHandler
へ渡します。
GameLift FlexMatchのマッチメーキングルールセットの作成
FlexMatchを利用するためには、どのようなルールでプレイヤー同士のマッチング処理を行うかを記載するマッチメーキングルールセットの作成と、FlexMatchをどのように運用するかの設定を行うマッチメーキング設定の作成が必要となります。
最初に、TestRuleSet
という名前で以下のマッチメーキングルールセットを作成します。
{
"name": "aliens_vs_cowboys",
"ruleLanguageVersion": "1.0",
"playerAttributes": [{
"name": "skill",
"type": "number",
"default": 10
}],
"teams": [{
"name": "cowboys",
"maxPlayers": 1,
"minPlayers": 1
}, {
"name": "aliens",
"maxPlayers": 1,
"minPlayers": 1
}],
"rules": [{
"name": "FairTeamSkill",
"description": "The average skill of players in each team is within 10 points from the average skill of all players in the match",
"type": "distance",
// get skill values for players in each team and average separately to produce list of two numbers
"measurements": [ "avg(teams[*].players.attributes[skill])" ],
// get skill values for players in each team, flatten into a single list, and average to produce an overall average
"referenceValue": "avg(flatten(teams[*].players.attributes[skill]))",
"maxDistance": 10 // minDistance would achieve the opposite result
}, {
"name": "EqualTeamSizes",
"description": "Only launch a game when the number of players in each team matches, e.g. 4v4, 5v5, 6v6, 7v7, 8v8",
"type": "comparison",
"measurements": [ "count(teams[cowboys].players)" ],
"referenceValue": "count(teams[aliens].players)",
"operation": "=" // other operations: !=, <, <=, >, >=
}],
"expansions": [{
"target": "rules[FairTeamSkill].maxDistance",
"steps": [{
"waitTimeSeconds": 5,
"value": 50
}, {
"waitTimeSeconds": 15,
"value": 100
}]
}]
}
上記の内容は、サンプルとして用意されているルールセットを元に作成しました。
プレイヤーのスキルレベルを元に、cowboysチームとaliensチームに均等にプレイヤーを配置するルールとなっています。今回の記事では具体的な記述内容の解説は行いませんが、teams
プロパティでチームの構成設定を書いており、rules
プロパティでマッチングのロジックを書いています。
サンプルでは、各チームのプレイヤー数は4~8に設定されていましたが、今回はマッチングをすぐに完了させるため1人チームに設定しています。
FlexMatchのマッチメーキング設定の作成
以下のような設定でTestMatchmakingConfig
という設定を作成しました。
- 設定名
- TestMatchmakingConfig
- ルールセット
- TestRuleSet
- FlexMatchモード
- STANDALONE
- リクエストのタイムアウト
- 30秒
- イベント通知設定
FlexMatchTopic
を指定
マッチメーキング設定では、上記の他にもプレイヤーによるマッチング承諾機能などの設定を行うことが可能です。
マッチングテスト
設定が完了したのでFlexMatchを実際に動かしてみます。
今回は、マッチング要求とそのステータスの確認をAWS CLI経由で行いました。
実際のゲームで使用する場合は、ゲームクライアントから各APIへリクエストを送る形になります。
マッチング要求
aws gamelift start-matchmakingコマンドで、JSON形式のプレイヤー情報を送信します。
aws gamelift start-matchmaking --cli-input-json '
{
"ConfigurationName": "TestMatchmakingConfig",
"Players": [
{
"PlayerId": "User1",
"PlayerAttributes": {
"skill": {
"N": 15
}
}
}
]
}
'
マッチング要求のリクエスト後にチケットの作成が完了すると、以下のようなレスポンスが得られます。これにより、今回のマッチング要求のチケットIDが把握できます。
{
"MatchmakingTicket": {
"TicketId": "8ad15a11-1beb-48ec-bf89-0baae1f6b2c6",
"ConfigurationName": "TestMatchmakingConfig",
"ConfigurationArn": "arn:aws:gamelift:ap-northeast-1:9999999999:matchmakingconfiguration/TestMatchmakingConfig",
"Status": "QUEUED",
"StartTime": "2023-11-18T07:06:00.499000+00:00",
"Players": [
{
"PlayerId": "User1",
"PlayerAttributes": {
"skill": {
"N": 15.0
}
}
}
]
}
}
マッチングステータスの確認
受け取ったチケットID情報を使って、API Gatewayのエンドポイントを使って現状のマッチングステータスを確認します。
curl https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/MatchingResult?id=8ad15a11-1beb-48ec-bf89-0baae1f6b2c6
以下のようなレスポンスが得られ、User1プレイヤーがcowboysチームに配置されていることと、現在のステータスはMatchmakingSearching
でまだマッチング処理の途中であることが分かります。
{
"time":"2023-11-18T09:26:47.591Z",
"players":[{"playerId":"User1","team":"cowboys"}],
"ticketId":"8ad15a11-1beb-48ec-bf89-0baae1f6b2c6",
"status":"MatchmakingSearching"
}
他のプレイヤーがいないとマッチングは完了しないので、追加で以下のUser2のマッチング要求を送ります。
aws gamelift start-matchmaking --cli-input-json '
{
"ConfigurationName": "TestMatchmakingConfig",
"Players": [
{
"PlayerId": "User2",
"PlayerAttributes": {
"skill": {
"N": 15
}
}
}
]
}
'
その後、User1のチケットIDを使って再度マッチングステータスを確認すると、以下のようにMatchmakingSucceeded
というステータスに変わっており、マッチングが完了したことが分かります。
{
"time":"2023-11-18T09:26:47.591Z",
"players":[{"playerId":"User1","team":"cowboys"}],
"ticketId":"8ad15a11-1beb-48ec-bf89-0baae1f6b2c6",
"status":"MatchmakingSucceeded"
}
なお、今回は必要最低限の情報しかLambda経由でDynamoDBに保存していないので上記のようなレスポンスとなりましたが、例えばFlexMatchをGameLift Managed Hostingと連携させている場合、マッチング成功イベント通知の中にゲームセッションの情報も入ってくるので、そこからゲームクライアントへどのサーバーへアクセスすれば良いのかを伝えることができます。
具体的なデータ構造の例については、以下のリンク先サンプルのgameSessionInfoプロパティを見ると確認が可能です。
FlexMatch matchmaking events - Amazon GameLift
なお、MatchmakingSearching
ステータスのままマッチメーキング設定で決めたタイムアウト時間を過ぎると、MatchmakingTimedOut
イベントが送信され、以降はそのチケットはマッチングに使用されなくなります。
注意点
上記のFlexMatchの検証時、マッチングステータスが素早く切り変わった際にイベント通知が正しい順番で送信されないことがありました。利用時の条件にもよりますが、必要に応じてSNSはFIFOトピックを活用することも視野に入れた方が良いかもしれません。
まとめ
GameLift FlexMatchのマッチングステータスを確認するアーキテクチャを構築し、実際に動作させてみました。
オンラインゲームのマッチング処理は、場合によってはかなり複雑なロジックを組む必要性があり、自分で最初から開発していくのは大変です。そのため、FlexMatchのような簡単に使用できてルールの拡張性も高いサービスは、ゲーム開発の効率を上げるためにかなり有用なものと思います。