Amazon Web Services ブログ

Amazon DynamoDB の使用を開始するためのゲーム開発者ガイド

多くのゲームでデータベースが不可欠の部分であることは周知の事実です。けれども、ゲーム開発者としては、データベースのエンジニアリングではなく、優れたゲームの構築にすべての時間と専門知識を捧げたいでしょう。

わかります。私も、データストレージについて考えるのではなく、コリジョンボリュームの修正、フレームレートの引き上げ、あるいは完璧な制御システムの作成に気をかけたいです。では、それを簡単に行いましょう。この記事では、AWS の高速で柔軟な NoSQL サービスである Amazon DynamoDB を使用して、ゲームにデータベースを追加するのがいかに簡単であるかを示します。

DynamoDB は、キーをデータに関連付ける非リレーショナルデータベースです。データベース、特にゲームでの使用に詳しくない場合は、ゲームでのデータベースの基本的な使用法と、リレーショナルデータベースと非リレーショナルデータベース (ときに SQL と NoSQL と呼ばれる) の違いについて説明したこの記事「素晴らしいゲームのためのマネージドデータベース」をご覧ください。

Dynamo DB テーブルの作成

DynamoDB は、NoSQL データベースですが、SQL データベースから借用した用語テーブルを使用します。この場合のテーブルは、実際にはデータベースに格納されるアイテムの主要な機能を定義したものにすぎません。

DynamoDB には、テーブルを識別するためのテーブルのプライマリキーが常にあり、このキーはデータベースに保存されているすべてのアイテムに対して一意である必要があります。

便利なヒントは、プライマリキーが複数の値のハッシュになる可能性があることです。したがって、単一の一意の属性がない場合は、異なる属性を組み合わせて一意の値を作成できます。たとえば、名前とバージョン番号を持つアイテムです。「broadsword」という名前のアイテムは 10 個あり、「version 1」のアイテムは 100 個ありますが、「broadsword version 1」は 1 個しかありません。

DynamoDB には、オプションで、アイテムをすばやく並べ替えることができるソートキーもあります。たとえば、プレイヤーのソートキーはプレイヤークラスであるかもしれません。これにより、ゲーム内のすべてのファイタークラスのリストを取得するクエリが高速化されることでしょう。データベース内のアイテムを一意に識別するための代替方法でするセカンダリキーを持つこともできます。

このデモでは、プレイヤーキャラクターの属性を表す非常にシンプルなオブジェクトを作成します。オブジェクトには一意のプレイヤー ID、プレイヤーレベル、強さと知力の 2 つの統計が含まれます。プレイヤー ID がプライマリキーになり、このデモ用のソートキーやセカンダリキーはありません。

注意: 自分で練習したい場合は、ソートキーとして機能するプレイヤークラスを追加できます。

DynamoDB テーブルを作成する手順は次のとおりです。

  1. AWS コンソールを開きます。
  2. DynamoDB を検索して選択します。
  3. DyanamoDB コンソールのホームページから [テーブルの作成] を選択します。
  4. テーブルに「PlayerData」という名前を付けます。
  5. プライマリキーを「PlayerID」に設定し、データ型を「string」のままにします。
  6. デフォルト設定を使用します。
  7. [作成] をクリックすると、コンソールが数秒間回転します。これで、Dynamo DB テーブルができました!

テーブルを作成するすべての手順を AWS SDK によってコードを介して実行できるため、サービスを自動的に作成できることを覚えておきましょう。

ゲームサーバーの作成

セキュリティ上の理由から、ゲームクライアントが DynamoDB と直接通信することは望ましくないことに注意してください。代わりに、DynamoDB にリクエストを送信するゲームサーバーを作成します。ゲームクライアントはゲームサーバーと通信し、その代わりに DynamoDB クエリを実行するように要求します。このモデルにより、プレイヤーが使用できるデータを簡単に制限できます。

簡単にするために、マシンで管理者認証情報ファイルを使用しました。このファイルの作成に関する情報はこちらにあり、認証情報の入力場所の詳細はこちらをご覧ください。実際には、認証情報ファイルを扱いたくないかもしれません。その場合は、この記事を参照して、Amazon Cognito の認証情報プロバイダーの使用について学んでください。

以下の手順を完了する前に、DynamoDB SDK をプロジェクトに追加する必要があります。NuGet (DynamoDB SDK は AWSSDKCPP-DynamoDB と名づけます) を使用するか、ソースをビルドしてライブラリをプロジェクトに追加できます。 詳細については、このリンクにアクセスしてください。この記事は、AWS SDK の使用に慣れていない場合にも読んでおくとよいでしょう。リンクされた記事で説明されているため、この記事では SDK の設定の一部を省略します。

最初のステップは、偽のプレイヤーデータを作成して、何か操作を行うことです。このサンプルには実際のプレイヤーがおらず、データベースに 1,000 ものエントリを入力したくないため、ランダムに生成されたプレイヤーをデータベースに入力する関数を作成しましょう。

そうするためのプロジェクトの完全なソースコードは、こちらにあります

    bool PopulateDatabases()
    {
        ... snip ...
        // Create a bunch of random characters
        const int NUMBER_OF_CHARACTERS_TO_CREATE{ 1000 };
		list<PlayerDesc> newPlayerChunk;
		for (int chrIdx{ 0 }; chrIdx < NUMBER_OF_CHARACTERS_TO_CREATE; ++chrIdx)
		{
            PlayerDesc newPlayer;
            newPlayer.id = GetPlayerIDForInt(chrIdx);
            newPlayer.level = GenerateRandomLevel();
            newPlayer.strength = GenerateRandomStat();
			newPlayer.intellect = GenerateRandomStat();

			newPlayerChunk.push_back(newPlayer);
			if (newPlayerChunk.size() == MAX_DYNAMODB_BATCH_ITEMS)
			{
				cout << "Sending player chunk to DynamoDB..." << endl;
				SendPlayerChunkToDynamoDB(newPlayerChunk);
                newPlayerChunk.clear();
            }
        }
		return true;
    }

このループでは、1,000 人のランダムなプレイヤーが作成されます。DynamoDB を 1,000 回呼び出すのは非効率なため、これは行いたくないでしょう。DynamoDB では、1 回の API 呼び出しで最大 25 個のコマンドをバッチ処理できます。そのため、生成されたプレイヤーを 25 のグループに分割し、データを DynamoDB に送信する関数に送信します。

ヒント: このコードは、バッチ呼び出しの実行方法を示すためにサーバーで記述されています。DynamoDB に対して多くの読み取りまたは書き込みを行う関数を作成する必要がある場合は、AWS Lambda 関数を作成してこれを行う必要があります。より効率的で、データ転送コストが発生しません!

	void SendPlayerChunkToDynamoDB(const list<PlayerDesc>& playerChunk)
	{
		vector<Aws::DynamoDB::Model::WriteRequest> writeRequests;
		for (const auto& chunkItem : playerChunk)
		{
			Aws::DynamoDB::Model::AttributeValue avID;
			avID.SetS(chunkItem.id);
			Aws::DynamoDB::Model::AttributeValue avLevel;
			avStrength.SetN(to_string(chunkItem.level));
            ... snip ...

			Aws::DynamoDB::Model::PutRequest putRequest;
			putRequest.AddItem(DATA_KEY_ID, avID);
            putRequest.AddItem(DATA_KEY_ID, avLevel);		
            ... snip ...

			Aws::DynamoDB::Model::WriteRequest curWriteRequest;
			curWriteRequest.SetPutRequest(putRequest);
			writeRequests.push_back(curWriteRequest);
		}

		Aws::DynamoDB::Model::BatchWriteItemRequest batchWriteRequest;
		batchWriteRequest.AddRequestItems(PLAYER_DATA_TABLE_NAME, writeRequests);
		... snip ...
    }

この関数のループでは、各プレイヤーの説明は、アイテムに格納するデータの表現である AttributeValue に格納されます。次に、属性値を関連付けるキーとともに、属性が PutRequest に追加されます。次に、PutRequest が WriteRequest に追加されます (これはバッチであるためです)。

ヒント: このデモでは、書き込みが成功すると想定しています。実際には、負荷がかかっている場合、DynamoDB は送信するすべての書き込みリクエストに対応できない場合があります。その場合、呼び出しは成功しますが、書き込まれていないアイテムのリストを受信します。リストを受信したら、このリストに目を通し、書き込みのために再度送信する必要があります。次に、その結果を確認し、すべてが書き込まれるまで続けます。これは、exponential backoff を使用して、呼び出しの間に時間を置いて行う必要があります。

   void SetPlayerAttribueValue(const string& ID, const string& attributeKey, int newValue)
   {
        Aws::DynamoDB::Model::UpdateItemRequest updateItemRequest;
        updateItemRequest.SetTableName(PLAYER_DATA_TABLE_NAME);

        Aws::DynamoDB::Model::AttributeValue avID;
        avID.SetS(ID);
        updateItemRequest.AddKey(DATA_KEY_ID, avID);

        string updateExpression = "SET ";
        updateExpression += attributeKey;
        updateExpression += " = :l";
        updateItemRequest.SetUpdateExpression(updateExpression);

        Aws::DynamoDB::Model::AttributeValue av;
        av.SetN(to_string(newValue));
        map<string, Aws::DynamoDB::Model::AttributeValue> attributeValues;
        attributeValues[":l"] = av;
        updateItemRequest.SetExpressionAttributeValues(attributeValues);

        auto outcome{ s_DynamoDBClient->UpdateItem(updateItemRequest) };
        ... snip ...
   }

システムで値が設定され、プレイヤーがゲームをプレイして力を上げたら、データベースを更新してその増加を反映する必要があります。これは、「更新式」を使用して行われます。ここでは、更新するテーブルを指定してから、更新するプレイヤーの ID を指定します。次に、変数として「:l」を使用して、式構文を使用して式を作成します。その後、変数に適切な値が入力されます。

注意: 過去に DynamoDB を使用したことがある場合は、直接更新を行う別の方法があったことに注意してください。この方法は、更新式を使用するために廃止されました。

    bool GetPlayerDesc(const string& ID, PlayerDesc& playerDesc)
    {
        Aws::DynamoDB::Model::QueryRequest queryRequest;
        queryRequest.SetTableName(PLAYER_DATA_TABLE_NAME);
        string conditionExpression{ DATA_KEY_ID };
        conditionExpression += " = :id";
        queryRequest.SetKeyConditionExpression(conditionExpression); //https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html
        Aws::DynamoDB::Model::AttributeValue avID;
        avID.SetS(ID);
        map<string, Aws::DynamoDB::Model::AttributeValue> attributeValues;
        attributeValues[":id"] = avID;
        queryRequest.SetExpressionAttributeValues(attributeValues);
        auto outcome{ s_DynamoDBClient->Query(queryRequest) };
        if (outcome.IsSuccess())
        {
            auto result{ outcome.GetResult() };
            ... snip ...
            auto item = result.GetItems()[0];
            playerDesc.id = item[DATA_KEY_ID].GetS();   // we already know this, just showing how to read it
            playerDesc.strength = stoi(item[DATA_KEY_STRENGTH].GetN());
            playerDesc.intellect = stoi(item[DATA_KEY_INTELLECT].GetN());
        }
        ... snip ...
    }

これは、データベースからデータを取得するクエリを実行した例です。更新リクエストと類似していることに注意してください。ここで、DynamoDB が満たそうとする条件が作成されます。この場合、条件は非常に単純で、指定された ID でそのアイテムを取得します。少なくとも、DynamoDB を使用してプレイヤー属性を保存するコンテキストでは、これが最も一般的なリクエストになります。このクエリは、プライマリキーまたはセカンダリキーのいずれでも使用でき、必要な並べ替えも指定できます。非キークエリの場合、データベース理論のセクションに記載されているように、スキャンリクエストを実行します。

最後に

ここにあるサンプルコードには、サーバーと、TCP 接続を介してサーバーにアクセスする最小限のクライアントを備えたビルド可能なデモがあります。クライアントが、サーバーにデータの取得と書き込みを指示します。クライアントから直接 DynamoDB への呼び出しを行うことは可能ですが、これはセキュリティの観点からは非常によくない慣行です。ここで示したデモが、すべてのアクセスをサーバーから行っているのはそのためです。

サンプルを楽しんでいただければ幸いです。ゲームで DynamoDB を使用してどのようなことを思いついたのかの話を聞きたいと思います。AWS subreddit をチェックして、何を手がけているかのかを教えてください。質問がある場合はご質問ください。

詳細と次のステップ

Amazon DynamoDB スタートページ

ブログ記事 – Amazon DynamoDB: ゲームのユースケースと設計パターン

チュートリアルを見る: Amazon DynamoDB を使用したゲームアプリケーションのデータモデリング

オンラインで操作する準備ができていませんか? DynamoDB をローカルで実行する方法に関する次の記事をお読みください

 

原文はこちらです。