電通総研 テックブログ

電通総研が運営する技術ブログ

GameLift活用でUnrealEngineゲームのマッチング基盤を構築する【Part1】

こんにちは!金融ソリューション事業部の孫です。
今回の記事では、GameLiftを用いたUnrealEngineゲームセッションのマッチング基盤の構築をご紹介します!
実施事項が多い為、Part1~3の3記事に分けて連載します。
また、GameLiftを用いたゲームセッションの作成、接続については前回の記事でご紹介しておりますので、そちらもご覧いただければ幸いです。

Part1である今回は、AWS GameLiftにおいてFlexMatchに関するコンポーネントの構築を行います。

Part2の記事はこちらです!
Part3の記事はこちらです!

はじめに

冒頭でGameLiftを用いてマッチング基盤を構築することを述べましたが、具体的にはGameLiftが提供するFlexMatch機能を利用します。
FlexMatch機能とは、カスタマイズ可能なマッチメイキングサービスです。
特徴として、下記が挙げられます。

  • 簡単にゲームにあったカスタムルールを複数作れる
  • サーバーとの間のレイテンシーを基準にマッチング可能
  • 時間と共にルールの条件を緩和することも可能
  • ユーザーによるマッチング結果の承諾機能
  • キューを使用してゲームセッションを効率的に配置

FlexMatchを使用することで、フレキシブルなマッチング処理を簡単に実現でき、マルチプレイヤーゲームをさらに楽しむことができます!

今回の検証では、GameLift以外他のAWSコンポーネント(Cognito、AmazonSNS、DynamoAPI Gateway、Lambda)も利用しています。説明をシンプルにする為、それらのコンポーネントの解説は割愛いたします。

マッチング基盤の構築手順

手順は、以下のとおりです。
【Part1】

  • 1.ゲームセッション保管用キューの作成
  • 2.マッチングイベント発行用Amazon SNSの作成
  • 3.マッチングイベント保管用Amazon DynamoDBの作成
  • 4.FlexMatchの構築

【Part2】

  • 5.プレイヤー管理用Amazon Cognitoの作成
  • 6.マッチング処理用のバックエンドAPIの作成

【Part3】

  • 7.UEクライアントへのAPI組み込み
  • 8.マッチング機能の検証

AWSアーキテクチャ

構築全体像は、以下のアーキテクチャ図をご参照ください。
今回はGameLiftが担当するゲーム用のクラウドサーバーの管理以外にも、以下の機能を実装します。

  • プレイヤーアカウントの作成、および認証認可管理(Cognito)
  • マッチングをコントロールするバックエンドサービス(Lambda関数、SNS
  • マッチング機能をクライアントから呼びだすAPIAPI Gateway
  • マッチングイベントとプレイヤーデータの一時保管(DynamoDB)

プレイヤーはまず Cognito 認証でアクセスキーを取得し、次にアクセスキーを用いてマッチング API にリクエストし、最後にマッチング結果を取得してマッチングを完了します。
マッチングは現在オンライン中のプレイヤーを対象に、事前に設定したルールセットに従って処理を行います。

使用環境/ツール

ゲームセッション保管用キューの作成

FlexMatchを使ってプレイヤーマッチングを行うにあたり、一時的なゲームセッションを保管するキューを作成することで、Fleet IQサービスの最大活用およびマッチング遅延の改善を図ることができます。

  • GameLift ダッシュボードページで、「ダッシュボード」⇒「キュー作成する」をクリックしてキュー作成ページを開きます

  • キュー名を入れた上、「キューの作成」ボタンを押して、キューの作成を完了します
    ※今回は検証のため、その他設定はデフォルト値のままで十分です
    ※キューの作成にあたり、事前にFleetの作成が必要です。具体的な手順は前回の記事をご参照ください
  • キュー名:「GameLift_PoC_Queue」としました

マッチングイベント発行用Amazon SNSの作成

AmazonSNS(Amazon Simple Notification Serviceの省略)の機能を利用することで、マッチング中にGameLiftが出力したイベントを非同期で拾ってくれます。
イベントによって現在マッチングのステータスを把握した上で、ネクストアクションを決めます。
イベントの種類については、以下が挙げられます。

  • MatchmakingSearching(マッチング中)
  • PotentialMatchCreated(マッチング候補が作成済「プレイヤーの承諾前」)
  • AcceptMatch(マッチング候補を承諾済)
  • AcceptMatchCompleted(プレイヤーの承諾・却下または承諾のタイムアウトにより、マッチングの承諾プロセス完了)
  • MatchMakingSucceded(マッチングが正常に完了し、ゲームセッションが作成済)
  • MatchMakingTimedOut(タイムアウトによってマッチング失敗)
  • MatchMakingCancelled(マッチングがキャンセル済)
  • MatchMakingFailed(マッチングでエラーが発生)

※Tips:マッチング処理をよりよくスムーズに実装するため、各イベントの返却文参照資料を覚えておくことをお勧めします

これから、AmazonSNSを作成します。

  • AWS マネジメントコンソールの検索窓口に「SNS」を入力して、SNSのトップページに移動します

  • 左側Menu「Topic」⇒「Topicの作成」をクリックして、Topicの作成画面が表示されます

  • 以下の内容を入力して、「Topicの作成」ボタンを押して、SNSを作成します
    • 名前:「FlexMatchEventNofications」としました
    • アクセスポリシー - オプション:アドバンストを選択し、以下のJSON文を文末に入力します。
    {
      "Sid": "__console_pub_0",
      "Effect": "Allow",
      "Principal": {
        "Service": "gamelift.amazonaws.com"
      },
      "Action": "SNS:Publish",
      "Resource": "arn:aws:sns:your_region:your_account:your_topic_name",
    }

マッチングイベント保管用Amazon DynamoDBの作成

今回の検証では、DynamoDBの役割として二つがあります

  • AmazonSNSによって転送されたGameLiftのイベントを保管する
  • オンライン中のプレイヤーIDやそのほかの必要な情報を一時的に預ける

これから、DynamoDBを作成します。

  • AWS マネジメントコンソールの検索窓口に「DynamoDB」を入力して、DynamoDBのトップページに移動します

  • 「テーブルの作成」をクリックして、イベント保管テーブルを作成します
    ※DynamoDBはNoSQLなので、データベースの作成がなくテーブルから作成します。
    • テーブル名:「MatchmakingTickets」としました
    • パーティションキー:「Id」としました
    • そのほかの設定:デフォルト値のまま

  • 上記と同様な手順で、オンライン中のプレイヤーデータを保管するテーブルを作成します

    • テーブル名:「Players」としました
    • パーティションキー:「Id」としました
    • そのほかの設定:デフォルト値のまま
  • 作成したテーブルのOverviewページを開いて「Table details」⇒ 「Manage TTL」をクリックして、TTL Attributeを追加します。
    ※DynamoDBに保存したデータは、基本は一時的なデータなので、TTLを設定することによって自動的に消してもらうことを図ります

    • TTL Attribute名:「ttl」としました

ここまで、GameLiftのイベント保管テーブルを作成しました。
これから、AmazonSNSと処理連動するLambda関数を作成します。
※Lambda関数の動作としては、AmazonSNSから受け取ったイベント情報をDynamoDBに保存します。

  • AWS マネジメントコンソールでLambdaのトップページに移動します

  • 「関数の作成」ボタンをクリックして作成ページを開きます

  • 以下の内容で関数を作成します
    • 関数名:「TrackEvents」としました
    • ランタイム:「NodeJs 18.x」としました

  • 関数コードについて、以下のソースコードを入力します
    • 関数の役割としては、GameLiftが発行されたイベントを受け取って中身(イベント番号、イベント種類、ゲームセッション、プレイヤー情報)をDynamoDBに保存する

TrackEventsコード

const AWS = require('aws-sdk');   
const DynamoDb = new AWS.DynamoDB({region: 'ap-northeast-1'});   

exports.handler = async (event) => {
    let message;
    let response;
    if (event.Records && event.Records.length > 0) {
        const record = event.Records[0];
        if (record.Sns && record.Sns.Message) {
            console.log('message from gamelift: ' + record.Sns.Message);
            message = JSON.parse(record.Sns.Message);
        }
    }

    if (!message || message['detail-type'] != 'GameLift Matchmaking Event') {
        response = {
            statusCode: 400,
            body: JSON.stringify({
                error: 'no message available or message is not about gamelift matchmaking'
            })
        };
        return response;
    }
    
    const messageDetail = message.detail;

    const dynamoDbRequestParams = {
        RequestItems: {
            MatchmakingTickets: []
        }
    };
    
    if (!messageDetail.tickets || messageDetail.tickets.length == 0) {
        response = {
            statusCode: 400,
            body: JSON.stringify({
                error: 'no tickets found'
            })
        };
        return response;
    }
    

    if (messageDetail.type == 'MatchmakingSucceeded' || messageDetail.type == 'MatchmakingTimedOut' || messageDetail.type == 'MatchmakingCancelled' || messageDetail.type == 'MatchmakingFailed') {
        for (const ticket of messageDetail.tickets) {
            const ticketItem = {};
            ticketItem.Id = {S: ticket.ticketId};
            ticketItem.Type = {S: messageDetail.type};
            ticketItem.ttl = {N: (Math.floor(Date.now() / 1000) + 3600).toString()};
            
            if (messageDetail.type == 'MatchmakingSucceeded') {
                ticketItem.Players = {L: []};
                const players = ticket.players;
                
                for (const player of players) {
                    const playerItem = {M: {}};
                    playerItem.M.PlayerId = {S: player.playerId};
                    if (player.playerSessionId) {
                        playerItem.M.PlayerSessionId = {S: player.playerSessionId};
                    }
                    
                    ticketItem.Players.L.push(playerItem);
                }
                
                ticketItem.GameSessionInfo = {
                    M: {
                        IpAddress: {S: messageDetail.gameSessionInfo.ipAddress},
                        Port: {N: messageDetail.gameSessionInfo.port.toString()}
                    }
                };
            }
            
            dynamoDbRequestParams.RequestItems.MatchmakingTickets.push({
                PutRequest: {
                    Item: ticketItem
                }
            });
        }
    }

    await DynamoDb.batchWriteItem(dynamoDbRequestParams)
    .promise().then(data => {
        response = {
            statusCode: 200,
            body: JSON.stringify({
                success: 'ticket data has been saved to dynamodb'
            })
        };
    })
    .catch(err => {
        response = {
            statusCode: 400,
            body: JSON.stringify({
                error: err
            })
        };
    });
    
    return response;
};

  • DynamoDBへのアクセスが必要なため、アクセス権限に追加します
    • 「設定」⇒「アクセス権限」で、対象ロールをクリックしてアクセス権限編集ページにとばされます
    • JSONタブの配下に、以下のJSON文を文末に追記します
{
    "Effect": "Allow",
    "Action": "dynamodb:BatchWriteItem"
    "Resource": "「関数のARN入力」"
}

ここまで、DynamoDBのテーブルとイベント登録用のLambdaを作成しました。

FlexMatchの構築

FlexMatch構築用のリソースを全部そろっているため、これから、FlexMatchルールセットの設定およびMatchmakingの設定を行います。

  • Matchmaking のルールセットを設定します
    • GameLiftのダッシュボードに移動し「マッチメーキングルールセットの作成」をクリックしてマッチングルール設定ページに移動します

  • ルールセットの設定ページで以下の内容を入力します
    • ルールセット名:「PocRuleSet」としました
    • ルールセット:
      • プレイヤーのチームを2つ作成します
      • 各チームに1名のプレイヤーを含めます
      • 各プレイヤーには所属グループ(groupid)という属性があります
      • 最終的に両チームのプレイヤー数、かつgroupidは同じにする必要があります

ルールセット内容

{
    "name": "poc_test",
    "ruleLanguageVersion": "1.0",
    "playerAttributes": [{
        "name": "groupid",
        "type": "number",
        "default": 1
    }],
    "teams": [{
        "name": "play1",
        "maxPlayers": 1,
        "minPlayers": 1
    }, {
        "name": "play2",
        "maxPlayers": 1,
        "minPlayers": 1
    }],
    "rules": [{
        "name": "EqualGroupId",
        "description": "Only launch a game when the group id of players in each team matches",
        "type": "comparison",
        "measurements": [ "teams[play1].attributes[groupid])" ],
        "referenceValue": "teams[play2].attributes[groupid])",
        "operation": "=" 
    },{
        "name": "EqualTeamSizes",
        "description": "Only launch a game when the number of players in each team matches",
        "type": "comparison",
        "measurements": [ "count(teams[play1].players)" ],
        "referenceValue": "count(teams[play2].players)",
        "operation": "=" 
    }]
}

  • 「ルールセットの作成」をクリックして作成します

  • Matchmaking Configurationを設定します

    • GameLiftのダッシュボードで「マッチメーキング設定の作成」をクリックしてマッチメーキングの設定ページに移動します

  • 以下の内容でマッチメーキングの設定を行います
    • 名前: 「GameLiftPOCMarchmaker」としました
    • キュー: ap-northeast-1、GameLift_PoC_Queue(手順1で作成したキュー名)
    • リクエストのタイムアウト:60
    • ルールセット名: PocRuleSet(前手順で作成したルールセット)
    • 通知先:手順2で作成したFlexMatchEventNoficationsのARN番号
    • そのほか:デフォルト値

Part1 FlexMatchに関するコンポーネントの構築は、以上となります!

Part2の記事はこちらです!
Part3の記事はこちらです!

現在ISIDはweb3領域のグループ横断組織を立ち上げ、Web3およびメタバース領域のR&Dを行っております(カテゴリー「3DCG」の記事はこちら)。
もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ぜひお気軽にご連絡ください!
私たちと同じチームで働いてくれる仲間を、是非お待ちしております!
ISID採用ページ(Web3/メタバース/AI)

執筆:@chen.sun、レビュー:@wakamoto.ryosuke
Shodoで執筆されました