電通総研 テックブログ

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

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

こんにちは!金融ソリューション事業部の孫です。
Part1の記事では、FlexMatchに関するコンポーネントの構築をご紹介しました!
さて、Part2である今回は、

  • プレイヤーの認証・管理用Cognitoの作成
  • UEクライアントの組込みに関するバックエンドAPIの作成

をご紹介します!

Part2の続きとして、Part3の記事はこちらです!

プレイヤー管理用Amazon Cognitoの作成

マッチング検証にあたり、複数プレイヤーの登録が必要になります。
その為、プレイヤーの認証、承認および管理をシンプルにする為、Amazon Cognitoを採用します。

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

  • 左側Menu「ユーザープール」⇒「ユーザープールの作成」ボタンをクリックして作成ページを開きます

  • Cognitoの設定は、以下の内容を入力します
    • Pool Name: 「GameLiftUnreal-UserPool」とします
    • 「Review Defauls」をクリックし、設定を確認した上で、プールを作成します
      ※Tips: ここまでの設定はプール作成後、変更できないため、ちゃんと確認してから前に進めてください
    • プールの作成完了後、左Menuに「App Clients」⇒ 「Add an app client」をクリックして以下の内容でAPPクライアントを作成します
      • App client name:「GameLiftUnreal-LambdaLoginClient」としました
      • 「Generate client secret」のチェックを外します
      • 「Auth Flows Configuration」: 「Enable username password based authentication」と「Enable refresh token based authentication」のみチェックします
      • そのほか:デフォルト値
    • 左menu「App Integration」⇒「App Client Settings」をクリックし設定を行います
    • 左menu「App Integration」⇒「Domain name」のDomain Prefix: 今回は「gameliftunreal-cog-20230222」 としました

ここまで、Cognitoの構築は完了しました。
次に、UEクライアントとCognito連携用のAPIを構築します。

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

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

  • 以下の内容で関数を作成します。
    • 関数名:「GameLiftUnreal-CognitoLogin」としました
    • ランタイム:「Python3.9」としました

  • 関数コードは、以下の内容を入力します
    • 関数の役割としては、プレイヤーが入力したユーザー名とパスワードをCognitoに送信し、その認証結果をプレイヤーに返却します

GameLiftUnreal-CognitoLoginコード

import boto3
import os
import sys

USER_POOL_APP_CLIENT_ID = 'Cognitoで生成したクライアントアプリへのアクセスキー'

client = boto3.client('cognito-idp')

def lambda_handler(event, context):
    if 'username' not in event or 'password' not in event:
        return {
            'status': 'fail',
            'msg': 'Username and password are required'
        }
    resp, msg = initiate_auth(event['username'], event['password'])
    if msg != None:
        return {
            'status': 'fail', 
            'msg': msg
        }
    return {
        'status': 'success',
        'tokens': resp['AuthenticationResult']
    }

def initiate_auth(username, password):
    try:
        resp = client.initiate_auth(
            ClientId=USER_POOL_APP_CLIENT_ID,
            AuthFlow='USER_PASSWORD_AUTH',
            AuthParameters={
                'USERNAME': username,
                'PASSWORD': password
            })
    except client.exceptions.InvalidParameterException as e:
        return None, "Username and password must not be empty"
    except (client.exceptions.NotAuthorizedException, client.exceptions.UserNotFoundException) as e:
        return None, "Username or password is incorrect"
    except Exception as e:
        print("Uncaught exception:", e, file=sys.stderr)
        return None, "Unknown error"
    return resp, None

  • Cognitoへのアクセスが必要なため、アクセス権限を追加します
    • 「設定」⇒「アクセス権限」で、対象ロールをクリックしてアクセス権限編集ページにとばされます
    • JSONタブの配下に、以下のJSON文を文末に追記します
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "cognito-idp:InitiateAuth",
            "Resource": "*"
        }

ここまでは、GameLiftUnreal-CognitoLogin関数を作成しました。
次に、API Gatewayにおいて、関数を外から呼びだすAPIを作成します。

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

  • RestAPIを選択し、「構築」ボタンを押してAPIの作成ページを開きます

  • 以下の内容でAPIを作成します

    • プロトコルを選択する:Rest
    • 新しい API の作成 : New API
    • 名前と説明
      • API名 : 「GameLiftUnreal-API」としました
    • その他:デフォルト値
      APIの作成」ボタンを押してAPIを作成しました。
  • 作成したAPIを開いて「アクション」⇒「リソースの作成」でloginリソースを作成します

  • 「アクション」⇒「メソッドの作成」でPOSTメソッドを作成します
    • 統合タイプ:lambda Functionチェック
    • Lambdaリージョン:ap-northeast-1
    • Lambda 関数:前手順で作成したGameLiftUnreal-CognitoLoginを選択

プレイヤーのIDとパスワードを入力した上で、ログインAPIにリクエストしたら、Cognitoからアクセスキーを発行してくれます。
そして、発行されたアクセスキーを用いて、他のAPIにも正常にリクエストができます。

マッチング処理用のバックエンドAPIの作成

上記AmazonSNS作成時に説明した通り、各マッチングイベントごとに後続処理の実装が必要なのですが、今回は検証をシンプルにする目的で「StartMatchMaking」イベントのみ処理します。
また、その他の補助処理として「①GetPlayerData:プレイヤーデータ取得」と「②PollMatchMaking:マッチング結果取得」も実装します。

  • バックエンドのLambda関数を作成します
    • 上記のLambda作成と同様な手順でLambda作成ページに移動し、以下の内容で関数本体を作成します
      1. StartMatchmaking
        • 関数の役割としては、マッチングで必要なデータをインプットしてマッチングを開始します
        • 関数名:「StartMatchmaking」としました
        • ランタイム:「NodeJs 18.x」としました
      2. GetPlayerData
        • 関数の役割としては、現在ログイン中のプレイヤー情報を取得します
        • 関数名:「GetPlayerData」としました
        • ランタイム:「NodeJs 18.x」としました
      3. PollMatchMakingのLambda関数定義
        • 関数の役割としては、マッチング結果を取得します(ポーリング方式)
        • 関数名:「PollMatchMaking」としました
        • ランタイム:「NodeJs 18.x」としました
    • 上記作成した関数本体を開いて、対象のソースコードを入力します

StartMatchmakingコード

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

exports.handler = async (event) => {
    let response;
    let raisedError;
    let latencyMap;
    
    if (event.body) {
        const body = JSON.parse(event.body);
        if (body.latencyMap) {
            latencyMap = body.latencyMap;
        }
    }
    if (!latencyMap) {
        response = {
            statusCode: 400,
            body: JSON.stringify({
                error: 'incoming request did not have a latency map'
            })
        };
        return response;
    }
    
    const lambdaRequestParams = {
        FunctionName: 'GetPlayerData',
        Payload: JSON.stringify(event)
    };
    
    let playerData;
    
    await Lambda.invoke(lambdaRequestParams)
    .promise().then(data => {
        if (data && data.Payload) {
            const payload = JSON.parse(data.Payload);
            if (payload.body) {
                const payloadBody = JSON.parse(payload.body);
                playerData = payloadBody.playerData;
            }
        } 
    })
    .catch(err => {
        raisedError = err;
    });
    
    if (raisedError) {
        response = {
            statusCode: 400,
            body: JSON.stringify({
                error: raisedError
            })
        };
        return response;
    } else if (!playerData) {
        response = {
            statusCode: 400,
            body: JSON.stringify({
                error: 'unable to retrieve player data'
            })
        };
        return response;
    }
    
    const playerId = playerData.Id.S;
    const groupId = parseInt(playerData.groupId.N, 10);
    
    const gameLiftRequestParams = {
        ConfigurationName: 'GameLiftTutorialMatchmaker',
        Players: [{
            LatencyInMs: latencyMap,
            PlayerId: playerId,
            PlayerAttributes: {
                groupid: {
                    N: groupId
                }
            }
        }]
    };
    
    console.log('matchmaking request: ' + JSON.stringify(gameLiftRequestParams));
    
    let ticketId;
    
    await GameLift.startMatchmaking(gameLiftRequestParams)
    .promise().then(data => {
        if (data && data.MatchmakingTicket) {
            ticketId = data.MatchmakingTicket.TicketId;
        } 
        response = {
            statusCode: 200,
            body: JSON.stringify({
                'ticketId': ticketId
            })
        };
    })
    .catch(err => {
        response = {
            statusCode: 400,
            body: JSON.stringify({
                error: err
            })
        };
    });
    
    return response;
};

GetPlayerDataコード

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

exports.handler = async (event) => {
    let response;
    let raisedError;
    let accessToken;
    
    if (event.headers) {
        if (event.headers['Authorization']) {
            accessToken = event.headers['Authorization'];
        }
    }
    
    const cognitoRequestParams = {
        AccessToken: accessToken
    };
    
    let sub;
    
    await Cognito.getUser(cognitoRequestParams)
    .promise().then(data => {
        if (data && data.UserAttributes) {
            for (const attribute of data.UserAttributes) {
                if (attribute.Name == 'sub') {
                    sub = attribute.Value;
                    break;
                }
            }
        } 
    })
    .catch(err => {
        raisedError = err;
    });
    
    if (raisedError) {
        response = {
            statusCode: 400,
            body: JSON.stringify({
                error: raisedError
            })
        };
        return response;
    }
    
    const dynamoDbRequestParams = {
        TableName: 'Players',
        Key: {
            Id: {S: sub}
        }
    };
    
    let playerData;
    
    await DynamoDb.getItem(dynamoDbRequestParams)
    .promise().then(data => {
        if (data && data.Item) {
            playerData = data.Item;
        } 
        response = {
            statusCode: 200,
            body: JSON.stringify({
                'playerData': playerData
            })
        };
    })
    .catch(err => {
        response = {
            statusCode: 400,
            body: JSON.stringify({
                error: err
            })
        };
    });
    
return response;
};

PollMatchMakingコード

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

exports.handler = async (event) => {
    let response;
    let ticketId;
    
    if (event.body) {
        const body = JSON.parse(event.body);
        if (body.ticketId) {
            ticketId = body.ticketId;
        }
    }
    
    if (!ticketId) {
        response = {
            statusCode: 400,
            body: JSON.stringify({
                error: 'incoming request did not have a ticket id'
            })
        };
        return response;
    }
    const dynamoDbRequestParams = {
        TableName: 'MatchmakingTickets',
        Key: {
            Id: {S: ticketId}
        }
    };
    
    let ticket;
    
    await DynamoDb.getItem(dynamoDbRequestParams)
    .promise().then(data => {
        if (data && data.Item) {
            ticket = data.Item;
        } 
        response = {
            statusCode: 200,
            body: JSON.stringify({
                'ticket': ticket
            })
        };
    })
    .catch(err => {
        response = {
            statusCode: 400,
            body: JSON.stringify({
                error: err
            })
        };
    });
    
    return response;
};

  • 各関数のアクセス権限を編集します
    • 1.StartMatchmakingの アクセスポリシーに以下の内容を文末に追加します
    {
      "Effect": "Allow",
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:your_region:your_account:function:GetPlayerData",
    },
    {
      "Effect": "Allow",
      "Action": "gamelift:StartMatchmaking",
      "Resource": "*",
    }
  • 2.GetPlayerDataの アクセスポリシーに以下の内容を文末に追加します
    {
      "Effect": "Allow",
      "Action": "Dynamodb:GetItem",
      "Resource": "arn:aws:dynamodb:your_region:your_account:table/Players",
    }
  • 3.PollMatchMakingの アクセスポリシーに以下の内容を文末に追加します
    {
      "Effect": "Allow",
      "Action": "Dynamodb:GetItem",
      "Resource": "arn:aws:dynamodb:your_region:your_account:table/MatchmakingTickets",
    }
  • API GatewayでのAPIの作成
    • 上記ログインAPIの作成と同様な手順でAPIの作成ページに移動し、以下の内容でAPIリソースを作成します
      1. getplayerdata APIリソースの作成
        • Resource Name:getplayerdata
        • Resource Path : /getplayerdata
        • その他:デフォルト値
      2. startmatchmaking APIリソースの作成
        • Resource Name:startmatchmaking
        • Resource Path:/startmatchmaking
      3. pollmatchmaking APIリソースの作成
        • Resource Name:pollmatchmaking
        • Resource Path:/pollMatchmaking
    • APIごとにメソッドを作成します
      1. getplayerdata APIのGETメソッド作成
        • 統合タイプ:lambda Functionチェック
        • Lambdaリージョン:ap-northeast-1
        • Lambda 関数:GetPlayerData
      2. startmatchmaking APIのPOSTメソッド作成
        • 統合タイプ:lambda Functionチェック
        • Lambdaリージョン:ap-northeast-1
        • Lambda 関数:StartMatchMaking
      3. pollmatchmaking APIのPOSTメソッド作成
        • 統合タイプ:lambda Functionチェック
        • Lambdaリージョン:ap-northeast-1
        • Lambda 関数:PollMatchMaking

  • APIごとに認証を設定します
    • 図のように各APIメソッドごとに「メソッドリクエスト」⇒「認可」にCognito認証を設定します

ここまで、CognitoおよびバックエンドAPIの作成は、以上となります!
Part3では、バックエンドAPIを用いてUEクライアントへの組込みおよびマッチングの検証を行います。

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

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