AWS for Games Advent Calendar 2022 3日目の記事です。
まえがき
みなさんは、Amazon GameSparks (Preview) というサービスをご存じでしょうか。簡単に説明するとインフラを意識せずにゲームバックエンドサービスを構築することができる AWS が提供しているマネージドサービスです。
従来のゲームバックエンドサービスは、EC2, ECS, EKS, Lambda などのコンピューティング関連のサービスと RDS, DynamoDB, ElastiCache などデータベース関連のサービスを組み合わせて構築することが一般的かと思います。Amazon GameSparks 対照的にはこれらのインフラを意識せずにフルマネージドでスケーラビリティの高いバックエンドサービスを構築することが可能で、ゲームを作る!という本質的な課題解決に集中できる点がゲーム開発者にとって嬉しいポイントです。
さて、今私はアドベントカレンダーで Amazon GameSparks をネタに何か書いてみるかと思ったのでそのモチベーションについても簡単に紹介します。Amazon GameSparks は2022/12月現在プレビュー版として提供されており、先月の11/17に待望のアジアパシフィック (東京) リージョンで利用できるようになりました。このアップデートにより日本のユーザー(ゲーム開発者)からのフィードバックにも期待されていることが伺えます。では私も黙ってはいません。もう少し触ってみて個人的な感想をこぼしたいなと思い行動に移した次第です。
そして、より多くの日本のゲーム開発者が Amazon GameSparks に触れ、有益なフィードバックを行い、Amazon GameSparks が成長することで世界中のゲーム開発者が最終的にハッピーになれる将来を想像しています。
Amazon GameSparks の基本
プレビュー段階での代表的な機能は下記です。
バックエンドサービスとしての機能
- Cloud Code : Cloud Code (JavaScript ES5.1) はイベントドリブンでバックエンドサービスのロジックを実装する Amazozn GameSparks における根幹の機能となっており、他のコンポーネントとのハブのような役割として機能します。クライアントとのやりとりの形式 (リクエストやレスポンスのフィールド) を定義します。例えばクライアントから送信されたゲームクリア時のスコアをデータストレージに保存し、なんらかのレスポンスを返却するといった処理を実装可能です。
- 認証/アイデンティティ :プレイヤーを識別するアノニマス認証 (ゲスト認証) の機能です。Amazon GameSparks の SDK は自動的に、接続時にプレイヤーの識別と認証を行います。他の認証方法については、今後のサービスの追加機能として検討されています。
- プレイヤーデータストレージ : プレイヤーのデータを保存するストレージ機能です。例えばプレイヤーのレベルやログイン時間など個別のプレイヤーのデータを保存できます。
- DynamoDB 統合 : Cloud Code から 外部の DynamoDB のデータに対して読み書きを行うことができます。例えばゲームにおけるグローバルデータ (アイテムカタログや、マップごとの固有情報など) を保存できます。
- AWS Lambda 統合 : Cloud Code から簡単に Lambda Function を呼び出すことができます。Lambda Function を経由することでネイティブに統合されていない他の AWS サービスとの連携に利用できます。
- リーダーボード : リアルタイムにプレイヤーのスコアに応じた昇順/降順でのソートを行い、上位プレイヤーの抽出や、プレイヤー順位の抽出を行うことができ、競争性のあるコンテンツ(ランキング機能)などを構築できます。
開発/オペレーションとしての機能
- テストハーネス : マネジメントコンソール上での Cloud Code のロジックを検証することができる機能です。都度の変更に応じてわざわざクライアントから実行する必要がなくなります。
- Generate Code : ゲームエンジンへの統合を容易にするために、Amazon GameSparks で定義した API インターフェースに沿ったライブラリコードを自動出力する機能です。
- Deploy Next Stage: Dev, Staging, Production など複数のステージに対して都度承認を行いながら順々にデプロイを行う機能です。(プレビュー段階では Dev ステージのみ利用可能)
参考: Amazon GameSparks でインフラを意識せずにゲームのバックエンドサービスを開発しよう
Amazon GameSparks はじめの一歩
まずは、ドキュメントやサンプルコードを動かしてみるところから始めることになります。
いくつか公開されている各種ドキュメントやサンプルをかき集めました。まず最初はこれらのリソースを眺めつつ、Amazon GameSparks の可能性に夢を膨らませます。
URL | 概要 |
---|---|
Amazon GameSparks Developer Guide | 基本的な紹介から各ユースケースごとの紹介が行われています。 |
Amazon GameSparks Service API Reference | デプロイなど Amazon GameSparks 自体のリソースを制御するための API リファレンスです。少し Amazon GameSparks を触ってみようという用途であれば気にしなくても大丈夫です。 |
Amazon GameSparks Extension API Reference | Cloud Code から操作する API のリファレンスのです。Cloud Code のエディタを使うだけではどのような API がそのようなインターフェースで生えているのかを把握するのは難しいので、このリファレンスは重宝します。 |
Amazon GameSparks Client API Reference | Amazon GameSparks のクライアントに位置するプログラムで利用する API のリファレンスです。現在では 対応している実行環境は Unity (C#) のみの対応となっています。 |
サンプルアプリ - Hello World | Hello World を Amazon GameSparks で出力するサンプルです。 |
サンプルアプリ - Button Blaster Game | ボタンをクリックした回数を Amazon GameSparks で保存しつつ、クリック回数をゲームクライアントに出力するサンプルです。基本的なリクエスト、レスポンスの流れを抑えるのに最適です。 |
Amazon GameSparks でインフラを意識せずにゲームのバックエンドサービスを開発しよう | 上記の Button Blaster Game を例に"日本語で丁寧に"紹介されている AWS のデベロッパー向けウェブマガジンの記事です |
Building a multiplayer game with Amazon GameSparks and Amazon GameLift | 専用ゲームサーバーのホスティングサービスである Amazon GameLift とゲームバックエンドサービスとして Amazon GameSparks を組み合わせてマルチプレイヤーゲームを作る参考実装です。AWS Lambda, DynamoDB との統合など実践的な内容にも触れつつ、より実用的なユースケースとして紹介されています。 |
もう少し触ってみよう
さて、基本は抑えましたがもう少し触ってみてより、Amazon GameSparks の可能性を探りつつ、使い勝手についてもみていきたいと思います。
今回は、サンプルアプリ - Button Blaster Game のコードを拡張しつつ、触ってみることにしました。Button Blaster Game は Unity シーン上に作成した突起物のような GameObject をクリックした回数を Unity シーン上に出力するサンプルです。
画像引用: https://docs.aws.amazon.com/gamesparks/latest/dg/button-blaster.html
このサンプルに対して下記の追加機能を実施します。
- プレイヤーは任意のニックネームを登録することができる。ニックネームは ゲームバックエンド側でバリデーションを行い保存される。
- ボタンをクリックした回数をリーダーボードにも保存し、クリックした回数を降順にソートした状態でランキング化しプレイヤーのランキングと、上位3プレイヤーのスコアを出力する。
追加機能実装後の見た目はこのようなイメージです。
ニックネームの登録機能を作る
Unity のシーン上に InputFieled と Button を配置し、Button を押下すると、Amazon GameSparks の Request を行い、入力値を送信します。Amazon GameSparks では入力値の内容を AWS Lambda を実行してバリデーションを行います。下記のようなアーキテクチャです。
今回はあえて、AWS Lambda でバリデーションを行っていますが、例えば Profanity リソース(NG ワード集) を扱う外部システムに対して HTTP 経由で利用しバリデーションを行う必要があるなど要件がある場合、Amazon GameSparks の Cloud Code ではなく、AWS Lambda に役割を委譲する必要など発生するでしょう。
まず、AWS Lambda ですが今回は適当に半角英数字4~8文字に収めるように程度のチェックにしていますが、下記のような Lambda Function をひとつ作成します。
export const handler = async(event) => {
const nickname = event.nickname;
var result = true;
// 半角英数字 4~8
const reg = new RegExp(/^([a-zA-Z0-9]{4,8})$/);
if (!reg.test(nickname)) {
result = false;
}
// TODO: 他の入力チェック
return {
result: result,
};
};
Amazon GameSparks が Lambda Function を実行できるようにするためには、IAM を使用してアクセス許可を与える必要があります。Amazon GameSparks では、ステージごとに IAM ロールが割り当てられているため、対象のステージの IAM ロールに対して、Lambda Function を実行する IAM ポリシーを適用します。下記は IAM ポリシーの例です。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RunNumberAdditionFunction",
"Effect": "Allow",
"Action": "lambda:InvokeFunction",
"Resource": "arn:aws:lambda:us-east-1:111122223333:function:NumberAddition"
}
]
}
Cloud Code にて、Request Message を作成します。"SetPlayerNickname" とでも名付けておきましょう。
- Request fields
- nickname (String) Required
- Response fields
- nickname (String) Required
実際に Unity からニックネーム文字列を受け付けて、AWS Lambda を実行してバリデーションを行い、結果次第でプレイヤーのストレージに保存するコードはこのようになります。
try {
// Validate Nickname
const res = GameSparks().Lambda("demo-202211-a-nickname-validator").Invoke(
{
"nickname": message.nickname,
}
);
GameSparks().Logging().Debug(JSON.stringify(res.Payload));
if (!res.Payload.result) {
throw new Error("Nickname Validate Error. Nickname '" + message.nickname + "'");
}
// Store player data
const data = GameSparks().CurrentPlayer().GetData(["Nickname"]);
data.Nickname = message.nickname;
GameSparks().CurrentPlayer().SetData(data);
return GameSparks().Messaging().Response({
"nickname": message.nickname,
});
} catch (e) {
return GameSparks().Messaging().Error(e.getMessage);
}
ポイントは、GameSparks().Lambda("Lambda Function").Invoke({...})
で Lambda Function を実行できるところ、GameSparks().CurrentPlayer().SetData({})
でプレイヤーのストレージに保存できるところでしょう。
加えて、登録したニックネームを参照するための Request Message も作成します。"GetPlayerNickname" とでも名付けておきましょう。
- Response fields
- nickname (String) Required
let nickname = GameSparks().CurrentPlayer().GetData(["Nickname"]);
return GameSparks().Messaging().Response({
"nickname": nickname.Nickname ? nickname.Nickname : "undefined",
});
では、これらのコードをテストハーネスを用いてテストします。
まず、"SetPlayerNickname" の入力値を {"nickname": "foo"}
にして実行すると、エラーが発生します。これは Lambda Function にて文字列を半角英数字4~8文字に収めるようバリデーションをおこなっているためです。
続いて、"SetPlayerNickname" の入力値を {"nickname": "hoge"}
にして実行すると、成功します。"GetPlayerNickname" も実行すると、ニックネームが "hoge" になっていることも確認できます。
以上ですが、簡単に AWS Lambda と GameSparks を統合できることが確認できました。動作確認に関してもテストハーネスが便利ですね!
リーダーボードを利用する
今回の追加要件として抑えたいポイントは3点
- ボタンをクリックした回数をリーダーボードにも保存
- クリックした回数を降順にソートした状態でランキング化しプレイヤーのランクを参照
- 上位3プレイヤーのスコアを参照
まずは、リーダーボードを作成するところから始まります。とは言っても、リーダーボードを識別するための名前と Order (降順、昇順)の設定を行うだけなので何も難しくはありません。今回は "ButtonPressCount" と名前を付けます。
続いて、ボタンをクリックした回数をリーダーボードにも保存処理を追加します。サンプルアプリ - Button Blaster Game の手順でも作成した "ButtonPressHandler" の Cloud Code に追記する形で 1行追記するだけです。これだけでリーダーボードに対して特定のユーザーのスコアを登録できます。
GameSparks().Leaderboard("ButtonPressCount").SetEntry(GameSparks().CurrentPlayer().Id(), data.ButtonPressed);
続いて、プレイヤーのランクを参照する処理ですが、これもサンプルアプリ - Button Blaster Game の "NotificationHandler" に対して追記する形になります。
let around = GameSparks().Leaderboard("ButtonPressCount").GetEntriesAround(GameSparks().CurrentPlayer().Id(), 0, 0);
let rank = around.Entries[0] ? around.Entries[0].Rank : -1;
GameSparks().Leaderboard("ButtonPressCount").GetEntriesAround(GameSparks().CurrentPlayer().Id(), 0, 0)
の実装により、プレイヤーのエントリー(ランクの情報)のみ抽出できます。.GetEntriesAround
は before, after を設定できるためプレイヤーの順位の前後5名のエントリーを抽出するなどの使い方も簡単に行うことができます。
let top3 = GameSparks().Leaderboard("ButtonPressCount").GetEntries(1, 3);
GameSparks().Logging().Debug("GetEntries top 3: " + JSON.stringify(top3));
let rankEntries = [];
for (let i = 0; i < top3.Entries.length; i++) {
let player = GameSparks().Player(top3.Entries[i].EntityKey).GetData(["Nickname"]);;
rankEntries.push({
Rank: top3.Entries[i].Rank,
ButtonPressCount: top3.Entries[i].Score,
Nickname: player.Nickname ? player.Nickname : "undefined",
});
}
最後に、上位3プレイヤーのスコアを参照するに関しては、GameSparks().Leaderboard("ButtonPressCount").GetEntries(1, 3);
により、1位~3位のプレイヤーを抽出しています。これまた非常に簡単にエントリーを取得できることがわかりました。一般的にはゲーム開発では Redis のソート済みセット型を用いたリーダーボードの実装などありますが、別途 Redis のサーバーが必要になったり、Redis のデータの揮発性に対してどうリカバリするかなど運用に一手間かかってしまいますが、Amazon GameSparks では本当に簡単に扱えるのがありがたいですね。
さて、サンプルアプリ - Button Blaster Gameでは、 "ButtonPressedNotification" という Notification の Event にてボタン押下時のレスポンスとしてボタンクリック数(buttonPressCount)を通知するようになっていますが、加えてプレイヤーのランクと上位3プレイヤーの情報を追加します。
特に上位3プレイヤーは、"プレイヤー名", "ランク", "スコア" の情報を3名分リストの構造で扱いたいところです。Amazon GameSparks では、"Shape" という型システムがあり、ユーザーはプリミティブな型を組み合わせてゲーム内に独自の型を定義できます。この仕組みを利用して下記のような構造を作成します。
RankEntry Struct (
ButtonPressCount Integer
Rank Integer
Nickname String
)
RankEntries List<RankEntry> ()
Shape を定義することにより、Notification のフィールドにて、"RankEntries" を選択できるようになりました。
そして、この Shape に対応したクライアント(Unity)で利用するライブラリコードですが、Generate Codeにより、自動生成することが可能です。Generate Code は Amazon GameSparks で定義した API インターフェースに沿ったライブラリコードを自動出力する機能です。
一部の抜粋になりますが、このようにコードの自動生成を行うことができました!
public sealed class RankEntry
{
[JsonProperty]
public int ButtonPressCount { get; }
[JsonProperty]
public string Nickname { get; }
[JsonProperty]
public int Rank { get; }
public RankEntry(
int ButtonPressCount,
string Nickname,
int Rank)
{
this.ButtonPressCount = ButtonPressCount;
this.Nickname = Nickname;
this.Rank = Rank;
}
public override string ToString()
{
return string.Concat(
$"{nameof(ButtonPressCount)}: { ButtonPressCount }", Environment.NewLine,
$"{nameof(Nickname)}: { Nickname }", Environment.NewLine,
$"{nameof(Rank)}: { Rank }");
}
}
public sealed class Notification
{
[JsonProperty]
public int buttonPressCount { get; }
[JsonProperty]
public int myRank { get; }
[JsonProperty]
public List<RankEntry> rankEntries { get; }
public Notification(
int buttonPressCount,
int myRank,
List<RankEntry> rankEntries)
{
this.buttonPressCount = buttonPressCount;
this.myRank = myRank;
this.rankEntries = rankEntries;
}
public override string ToString()
{
return string.Concat(
$"{nameof(buttonPressCount)}: { buttonPressCount }", Environment.NewLine,
$"{nameof(myRank)}: { myRank }", Environment.NewLine,
$"{nameof(rankEntries)}: { rankEntries }");
}
}
簡単にリーダーボードを利用できる点や、コードの自動生成など便利な機能を体験することができました!
あとは、またテストハーネスを用いて動作確認が行えますので、動作確認あとにクライアント(Unity)統合します。が、今回の記事では時間の都合上省略します。
触ってみてよかったポイント
- AWS Lambda 統合により実質可能性は無限大であることは心強いです。この手のマネージドサービスは運用における開発者の手間が削減される点は嬉しいですが、拡張性に関しては損なわれてしまうことが多く、その点 AWS Lambda を接着剤のように利用することで例えば、RDS に保存されているデータの操作や、SQS へのメッセージキューイングを行えたり、はたまた Lambda から HTTP リクエストを行うことで AWS 外のシステムとも連携することができる。しかし、できる限りは Amazon GameSparks で補いつつ、Lambda は最終手段として利用することがシンプルさを保ちつつ、余計なコストを発生させないために注意すると良い印象を受けます。
- リーダーボード機能のようなちょうど良い抽象度の機能が Amazon GameSparks ネイティブに利用できることは嬉しい。他にも昨今のオンラインゲームで必須な機能はいくつかあります(フレンド、決済、インベントリー)が、そういった機能がどう今後提供されるのかという点は非常に興味深いです。
- Unity (クライアント)との統合が非常に簡単。ユーザーが定義した API インターフェースに沿ったライブラリコード(C# のソースコード)を自動出力することができ、それをそのまま Unity プロジェクトにホイっとドラッグ&ドロップして追加するだけで統合できるのは非常に開発体験がよかった。
- テストハーネスを利用することにより、Unity (クライアント)との統合をしなくてもほとんどの動作確認を行うことができる点が開発生産性で良い。
改善に期待したいポイント
- テストハーネス が Snapshot を Deploy しないと使えない。私個人としては Snapshot を Deploy する前に動作確認を行いたいのでこれは開発者体験上改善の余地があると感じました。実際 Snapshot を Deploy するにあたって反映に1,2min ほどの時間がかかってしまい待ち時間もそれなりに発生してしまいます。Cloud Code でコードを書く => テストハーネスで動作確認 => 問題ないこと確認できたら Snapshot Deploy のような流れが作れると開発者体験が向上しそうです。
- デバッグ目的に Cloud Code のエディタ上で breakpoint を張りたくなります。これも開発者体験の観点ですが、原因不明のバグを作ってしまった際に現状ではログを出力(ログは CloudWatch Logs に出力されます。)し変数の状態などを確認するしか術がないです。テストハーネスで一連の操作を行うのですが、上記の通りで Snapshot Deploy の手間もありイテレーションを回すのが非効率で結果、バグの原因を特定するのに時間がかかってしまいます。
- Cloud Code におけるソースコードの管理。現在 Cloud Code は Amazon GameSparks のエディタ上で直接テキスト入力することを基本としていますが、Git のようなバージョン管理ツールとの統合や、CI/CD パイプラインの自動化がプロダクションでの利用を想定すると欲しく思いました。正式版リリース時点でどのような手段を取れるのかは気になるところです。
Amazon GameSparks を利用したいユースケース
そんな Amazon GameSparks ですが、現在プレビュー版ということもあり本格的なオンラインゲームのバックエンドサービスとして利用するにはまだ不足感があることは否めないです。しかし、特定のユースケースであれば現在のプレビュー版で提供される相当の機能でも十分に役割を果たせると考えています。これは私個人の感想であり、人それぞれ解釈に違いはあると思いますので、優しい目でみてくれたら幸いです。
ユースケース: カジュアルゲームなど小規模なゲーム
カジュアルゲームのような比較的小規模なゲームにおいては、簡単なプレイヤーの状態の保存とリーダーボードによる複数プレイヤーでのスコアの競争くらいの機能があれば十分であることもあると思いますので、その点でフィットするでしょう。
また、カジュアルゲーム以外にも、展示目的のゲーム、ハッカソン、ゲームジャムなど小規模で良いのでゲームバックエンドが必要になるケースは多々あるとおもいます。その際の選択肢としてゲーム開発者は Amazon GameSparks を選択できることは強力な武器です。
ユースケース: プリプロダクション/アルファ開発フェーズ
所属する企業やチームによるとは思いますが、昨今のゲーム開発の現場では"プリプロダクション","アルファ"などといった開発のフェーズを定義し、プロトタイピングや企画に意識を集中させ、ゲームの本質的な面白さを追求するフェーズがあることが多いです。
この時、ゲームバックエンドは必要最低限の実装でよく、作業優先度も落ちていることがよくあります。いわゆるサーバーサイドエンジニアがチームに存在しない場合もあるでしょう。そんな中でも、ゲームの本質的な面白さを表現する上で、ゲームバックエンドを最低限実装したいこともあります。そこで、Amazon GameSparks です。Amazon GameSparks はインフラの調達も不要ですし、仮想マシン、 データベースなどのミドルウェア、アプリケーションフレームワークなどは不要です。Cloud Code (JavaScript) をちょっと書けば必要最低限のゲームバックエンドが作れます。これは、ゲームバックエンドに精通しているサーバーサイドエンジニアの工数も必要としません。※ サーバーサイドエンジニアは必要最低限の実装ではなく、より高度な技術が必要な問題に集中することができます。
ユースケース: 一部の機能だけ利用する
メインとなるゲームバックエンドは従来の EC2 + RDS などの構成で構築し、一部の機能だけ Amazon GameSparks を利用することも検討できるでしょう。"プレイヤーの認証、リーダーボードは Amazon GameSparks で、それ以外は従来の構成で" など都合よく選択し面倒臭いことを Amazon GameSparks にオフロードをすることで開発期間の短縮や、運用コストの削減に期待できます。AWS for Games Advent Calendar 2022 1日目 マイクロサービスとゲームでも、マイクロサービスについて語られていますが、1つのゲームを構築するにあたって、"1つのモノリスなサービス(ゲームバックエンド)で作るべきである。" ということはありません。適材適所に分割することを検討し、それによってメリットを得ましょう。分割されたサービスを Amazon GameSparks にオフロードできれば分割によるメリットを受けることができるでしょう。
Amazon GameSparks にフィードバックしよう!
Amazon GameSparks への要望などはフィードバックという形で手軽に送信できます。(フィードバックのリンクをポチっとすれば、簡単なフォームが出てきます。)
積極的にフィードバックを行うことにより、世界中のゲーム開発者がよりハッピーなる未来をみんなで作っていきましょう!
さいごに
Amazon GameSparks は現在プレビュー段階でありこのサービスには変更が加えられる可能性があります。
(免責) 本記事の内容はあくまでも個人の意見であり、所属する企業や団体は関係ございません。