Amazon Web Services ブログ

FlexMatchマッチング状態を確認するためのサーバレスアプリケーションの実装例

マッチメイキングシステムでは、セッションベースのマルチプレイゲームにおいて重要な1つの要素であり、良いマッチメイキングの結果は良いゲーム体験に繋がります。しかし、マッチングの速度、接続先に対するレイテンシー、マッチングの条件設定など考慮するべき要素が多く、実装工数がしばしば問題になります。Amazon GameLift の FlexMatchを使用すれば誰でも堅牢なプレイヤーマッチメイキングをすばやく簡単に作成できます。大人数のマッチメイキングでもレイテンシーが最も少ないサーバーインスタンスで、最大 200 人のプレイヤーを接続させることができます。

FlexMatchを利用する際にプレイヤーはマッチング状態(成功、失敗)を確認する必要があります。今回では、マッチング状況を確認できるサーバレスアプリケーションの実装例を紹介したいと思います。

アーキテクチャ

今回の構成では、マッチング状況のイベント通知をGameLiftからSNS経由しLambdaで処理した後、DynamoDBに保存します。ゲームクライエントはAPI Gatewayを経由し、Lambdaで現在のマッチング状況を確認します。アーキテクチャはゲームクライエントとバックエンドサーバ間にコネクションが確立されていないことを想定した構成です。もしバックエンドサーバから直接クライエントにプッシュ通知を送ることが可能の構成の場合、DynamoDBに保存せず、マッチングの情報をSNS経由でバックエンドサーバのHTTPエンドポイントを受付、ゲームクライエントに直接送信するシンプルな構成も考えられます。またモバイル端末なら、SNSから直接プッシュすることも可能です。詳細はこちらのドキュメントに参照してください。

マッチング結果の保存

まずマッチング状況のイベント通知をDynamoDBに保存するまでの設定を行いましょう。

SNSを設定

こちらのドキュメントを参照して新規SNS Topicを作成、GameLift の関連権限を JSON に追記します。

GameLiftと連携するには、GameLiftのコンソール画面から、”マッチメイキング設定の作成を選択肢“、‘通知先“のところで設定することが必要です。本記事では、SNSからメッセージの発行を行い、全体をテストするため、GameLiftとの連携は行わなくても大丈夫です。

DynamoDBの設定

  1. DynamoDBのコンソールを開き、”テーブル作成”をクリックします。
  2. テーブル名は先程環境変数に入れたmatchmakingを設定し、プライマリキーはticketIdを設定します。
  3. ”作成”をクリックし、テーブルを作成します。

今回はdefaultの設定になりますが、本番ゲームで利用する際に十分なキャパシティを設定する必要があります。ゲームリリース時はプレイヤーの数は予測困難の場合、キャパシティーモードをオンデマンドに変更することがおすすめです。オンデマンドキャパシティーモードの詳細についてこちらのドキュメントを参照してください。

Lambda関数の作成と関連の権限設定

AWS Lambda関数とは、ユーザによるサーバー管理が不要なAWSサーバーで実行されるコードです。GameLiftからSNSの通知をLambdaで受けて、マッチング成功しましたら、DynamoDBに保存します。
詳細の手順は以下のようになります。

  1. Lambdaのコンソールを開き、”関数の作成”をクリックします。
  2. “一から作成”を選択します。
  3. 関数の名前に”FlexMatchEventHandler”を入力します。
  4. ランタイムで”Python 3.8″を選択します。
  5. アクセス権限で”実行ロールの選択または作成”を表示し、”基本的なLambdaアクセス権限で新しいロールを作成”が選択されていることを確認します。
  6. “関数の作成”をクリックします。
  7. Lambdaエディタが開きます。Lambda関数がDynamoDBにアクセスできるか確認する必要があります。アクセス権限のタブを開き、”実行ロール”セクションがあることを確認します。
  8. 新しく作成されたロールは”service-FlexMatchEventHandler-role-abc1defg”のような名前です。そのロールリンクをクリックします。
  9. 新しいウインドウもしくはタブでロール設定のIAMのページが開きます。アクセス権限タブでポリシーのリストが確認できます。そこにはすでに”AWSLambdaBasicExecutionRole”が存在します。DynamoDB へのアクセスを許可するために2番目のポリシーを追加する必要があります。
  10. “ポリシーをアタッチします”をクリックします。そのリンクを新しいウインドウもしくはタブで開くことをおすすめします。あとで本ページに戻ってきます。
  11. “ポリシーの作成”をクリックします。
  12. ビジュアルエディタを利用します。(JSONのテンプレートを提供することもできましたが、いろいろなAPIが利用可能で、それらがどのように構築されているかを確認いただくのも興味深いと思います)
  13. “サービスの選択”をクリックします。
  14. DynamoDBを検索し、結果をクリックします。
  15. アクセスレベルで”読み込み”を展開し、”GetItem”をチェックします。
  16. “書き込み”を展開し、”BatchWriteItem”をチェックします。今回のLambdaではnofiticationを受付、成功したチケットを一気に書き込む機能を持ち、本来であればBatchWriteItemのみが必要になりますが、その後ゲームユーザからTicketIdを用いて現在のマッチング結果的を検索する機能もあるのでGetItemも付与しました。AWSのベストプラクティスとしてIAM権限は最小限に留めておくことをおすすめしますが、もしLambda 関数で他のDynamoDBの操作(QueryやScan)を実装する予定があれば、関連する権限付与を忘れないでください。
  17. リソースはすべてのリソースを指定します。
  18.  “ポリシーの確認”をクリックし、ポリシーの名前に“MatchingEventDynamoDBServicePolicy”と入力します。完了後、”ポリシーの作成”をクリックします。
  19.  そして、IAMロールのアクセス権限のページに戻ります。画面を更新し、新しいポリシーである”MatchingEventDynamoDBServicePolicy”を検索し、選択、”ポリシーのアタッチ”をクリックします。
  20. Lambda関数にコードを追加します。Lambdaエディタのページに戻り、以下のコードを関数コードのエディタにペーストします。元々存在していたコードもすべて上書きします。
import json
import logging
import boto3
import os
from botocore.exceptions import ClientError

# Set Logging Level
logger = logging.getLogger()
logger.setLevel(logging.INFO)


def lambda_handler(event, context):
    message = event['Records'][0]['Sns']['Message']
    body = json.loads(message)
    if 'detail' not in body or 'type' not in body['detail']:
        logger.error('wrong notification, check whether your notification is from GameLift flexmatch')
    event_type = body['detail']['type']

    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table(os.environ["MATCHING_TABLE"])
    if event_type == "MatchmakingSucceeded":
        try:
            with table.batch_writer() as batch:
                for ticket in body['detail']['tickets']:
                    logger.info(ticket)
                    batch.put_item(
                        Item={
                            'ticketId': ticket['ticketId'],
                            'players': ticket['players'],
                            'ipAddress': body['detail']['gameSessionInfo']['ipAddress'],
                            'port': body['detail']['gameSessionInfo']['port'],
                            'time': body['time']
                        }
                    )
        except ClientError as e:
            logger.error(e)
    elif event_type == "MatchmakingTimedOut":
        logger.info("Removed from matchmaking due to timing out")
    elif event_type == "MatchmakingCancelled":
        logger.info("Cancelled by request")
    elif event_type == "MatchmakingFailed":
        logger.error("An unexpected error was encountered during match placing.")
        logger.error(body)
    else:
        logger.info("Other event type")
    return message

21.今回はMatchingの結果成功した場合だけDynamoDB書き込みを行うシンプルなLambda関数になっています。FlexMatchから通知イベントは「マッチング開始」、「マッチング候補が作成された」や「マッチング成功・失敗」など、全部8種類があります。今回はMatchingの結果成功した場合だけDynamoDB書き込みを行っていますが、他のタイミングも記録したい場合は、その処理を追加することが必要です。イベントのフォーマットと通知タイミングは、こちらのドキュメントを参照してください。
22.Lambda内のDynamoDBテーブル名を環境変数から読み込みます。コードエディタの下に環境変数のブロックで、“編集”をクリック。
23.“環境変数の追加“をクリックし、KeyをMATCHING_TABLE、Valueをmatchmakingを入力し、保存します。
24. エディタの右上の“保存”をクリックすれば、Lambdaの部分は準備完了になります。

SNSとLambdaの連携設定

  1. Amazon SNSのコンソールを開き、先程作成したSNSトピックを開きます。
  2. “サブスクリプションの作成”をクリックします
  3. プロトコルはLambdaを選択します。
  4. エンドポイントは先程作成したLambdaのエンドポイントを選択します。
  5. サブスクリプションを作成します。
    作成後、FlexMatchEventHandlerのLambda関数に戻りますと、snsのトリガーが追加されたことが確認できます。

 

結果確認

  1. 作成されたSNSのトピックのコンソール画面から、”メッセージの発行“をクリックします。
  2. 下記のリンクでMatchmakingSucsssのサンプルjsonをコピーし、エンドポイントに送信するメッセージ本文のところに貼り付けます。
    https://docs.aws.amazon.com/ja_jp/GameLift/latest/developerguide/match-events.html#match-events-matchmakingsucceeded-example
  3. DynamoDBのコンソールを開き、マッチング成功したチケットが保存されたことを確認できます。

Serverless Applicationでマッチング状況を確認

まずマッチング状況のイベント通知をDynamoDBに保存するまでの設定を行いましょう。APIGatewayとLambdaを利用し、DynamoDBに保存します。
サーバレスのアプリケーションについて、より詳細を確認したい場合は、こちらの資料を参照してください

Lambda関数の作成と関連の権限設定

先程と同様の手順で、Lambda関数を作成することができますので詳細手順は割愛しますが、差分を説明します。

  1. step3関数の名前は設定部分は、”MatchingResultHandler”を入力します
  2. 先程作成したDynamoDBポリシーは流用できるため、Step 10~18は不要です。
  3. 環境変数の設定は上記と同様になります。KeyをMATCHING_TABLE、Valueをmatchmakingを入力し、保存します。
  4. 今回Lambda functionのコードは下記の内容を利用します。下記のlambda関数はticketIdを用いて、DynamoDBで検索するシンプルな実装になります。
import json
import logging
import boto3
import decimal
import os
from botocore.exceptions import ClientError

# Set Logging Level
logger = logging.getLogger()
logger.setLevel(logging.INFO)


def decimal_default(obj):
    if isinstance(obj, decimal.Decimal):
        return int(obj)
    raise TypeError


def server_response(status_code, body):
    return {
        'statusCode': status_code,
        "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.dumps(body, default=decimal_default)
    }


def lambda_handler(event, context):
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table(os.environ["MATCHING_TABLE"])
    if event['queryStringParameters'] is None or 'id' not in event['queryStringParameters']:
        return server_response(200,"Parameter Error")

    ticket_id = event['queryStringParameters']['id']
    try:
        response = table.get_item(
            Key={
                'ticketId': ticket_id
            }
        )
    except ClientError as e:
        logger.error(e.response['Error']['Message'])
    else:
        if "Item" in response:
            item = response['Item']
            return server_response(200, item)
        else:
            return server_response(200, 'Match not available')
 
       

API Gatewayの設定

  1. Lambdaのコンソールを開き、”API作成”をクリックします。
  2. REST APIを選択します。
  3. プロトコルは“API”を選択し、新しいAPIの作成では新しいAPIを選択します。
  4. API名を“matchevent”を入れ、“API作成”をクリックします。
  5. アクション”、“メソッドの作成”をクリックし、Getのメソッドを作成します。
  6. “Lambda プロキシ統合の使用“を有効し、lambda関数は先程作成したMatchingResultHandlerを入力します。
    作成後、MatchingResultHandlerのLambda関数に戻りますと、ApiGatewayのトリガーが追加されたことは確認できます。
  7. “アクション”、”APIのデプロイ”を選択し、“新しいステージ”を選びます。
  8. ステージ名を”test“を入れて、APIをデプロイします。
    成功したら下記のようにtestのステージのURLが生成されます。

結果確認

先程生成されたURLの後ろに“?id=ticket-1“をつけて、ブラウザでアクセスしてみると、先程保存されたチケット情報が確認できます。

{"port": 10777, "ipAddress": "192.168.1.1", "time": "2017-08-09T19:59:09.159Z", "players": [{"playerId": "player-1", "playerSessionId": "psess-6e7c13cf-10d6-4756-a53f-db7de782ed67", "team": "red"}], "ticketId": "ticket-1"}

id=ticket-3に変更すると成功したマッチングの中に存在しないため、”Match not available”が表示されます。
これでマッチングイベントの保存と読み込みも実装完了です。

最後

今回はFlexMatchイベント通知のサーバレスによる実装例を紹介しました。ログの処理、失敗時の処理、データの処理関連はかなり省略した形のサンプルのため、本番運用ではその周りのロジックも細かく設定する必要があります。例えば、クライエントにnotificationが届かない時のリトライ処理、マッチングのログ分析、マッチングのデータ管理など考えられます。詳細は割愛しますが、もし困ったことがあれば、Solutions Architectにお気軽にご相談いただければと思います。また、個別相談会も受け付けております。お気軽にお問い合わせください。

ソリューションアーキテクト Fan Liang