GS2-Account
Game Server Services が提供するアカウントシステムは「匿名アカウント」という種類のアカウントシステムです。 日本ではメジャーな仕組みですが、多くの地域では奇妙に感じるアカウントシステムかもしれません。
しかし、このアカウントシステムはゲームにおいて非常に理にかなったアカウントシステムです。
匿名アカウントとはなんですか?
通常イメージするアカウント管理は、ログインIDがありパスワードがあるものでしょう。 匿名アカウントもその点は変わりません。
しかし、ログインIDとパスワードの両方がシステムによってランダムに決定されるというのが最大の特徴です。
システムによって発行されたログインID・パスワードをデバイスのローカルストレージに保存し、2回目以降はその情報を利用してログインすることでゲームを再開できます。
graph TD Startup --> LoadSave{"デバイスストレージから\n匿名アカウントを読み込み"} LoadSave -- Not Exists --> CreateAccount["匿名アカウントを作成"] CreateAccount --> SaveAccount["匿名アカウントを保存"] SaveAccount --> Login["ログイン"] LoadSave -- Exists --> Login Login --> InGame
匿名アカウントのメリット
- 簡単な登録:ユーザーは煩雑な登録プロセスを経ずにゲームをすぐに開始することができます。特に、無料で遊べるF2Pスタイルのゲームでは、多くのユーザーがゲームを試してもらうためには登録の手間を減らすことが重要です。
- プレイヤー情報の保護:ログインIDやパスワードを自分で設定する必要がないため、個人情報やプライバシーが保護されます。また、パスワードの再設定や変更なども必要ありません。
- 引き継ぎのしやすさ:引き継ぎ情報に各種プラットフォームのID基盤やSNSのアカウントなどを利用することができ、異なるデバイスやアプリ間での引き継ぎがスムーズに行えます。
- 匿名でのプレイ体験:プレイヤーは匿名でゲームをプレイすることができるため、リアルネームやニックネームを使用しなくてもよく、より自由なプレイ体験を楽しむことができます。
引き継ぎ
デバイスのローカルストレージほど信頼性の低い場所は他にはありません。 デバイスを落としてしまうかもしれませんし、デバイスを壊してしまうかもしれません。 そのとき、ゲームデータを全て失ってしまうとしたら、それは絶望的な状況です。
ゲームプレイヤーは、匿名アカウントでゲームを体験し、本当に気に入ったら「引き継ぎ情報」を登録できます。 引き継ぎ情報には各種プラットフォーマーのID基盤の情報を利用してもいいですし、SNSのアカウントを設定してもいいでしょう。ゲームパブリッシャーのID基盤も良さそうです。
引き継ぎ情報には各種ID基盤で認証した結果得られるユーザーIDなどを記録します。 そして、全く新しいデバイスで引き継ぎを実行します。各種ID基盤にログインし、得られたユーザーIDで引き継ぎを実行すると 過去に引き継ぎ情報を設定した匿名アカウントでログインするための情報を取得することができます。
この情報を再びデバイスのローカルストレージに保存して、今後のログインに利用します。
graph TD InGame -- ゲームを気に入った --> AddTakeOverSetting["引き継ぎ情報を登録"] AddTakeOverSetting --> BrokenDevice["スマホが壊れた"] NewDevice["新しいスマホ"] --> DoTakeOver["引き継ぎ情報を入力して引き継ぎを実行"] DoTakeOver -- 匿名アカウントを復元 --> SaveAccount["匿名アカウントを保存"] SaveAccount --> Login["ログイン"]
OpenID Connect 連携
引き継ぎ情報の登録、引き継ぎ処理の実行に OpenID Connect に準拠した認証システムを使用するためのサポートが用意されています。
認証サービスの登録
Open ID Connect 連携機能を使用するにはマスターデータの設定が必要です。 設定項目は
- 認証サービスの OpenID Connect Discovery に基づくスペックURL
- 認証サービスのクライアントID
- 認証サービスのクライアントシークレット
が必要です。
Sign in with Apple は クライアントシークレット を動的に計算する必要がありますが、この計算機能も備えています。 OpenID Connect Discovery に基づくスペックURL に Sign in with Apple のURLを指定した場合は、クライアントシークレットの代わりに
- Apple Developer のチームID
- Apple から発行された秘密鍵のID
- Apple から発行された秘密鍵のペイロード(PEM)
を登録でき、クライアントシークレットは GS2 が必要に応じて計算します。
認証処理
認証処理に関するサポートも提供しています。
認証サービスの認証ページのURL取得APIや、 認証サービスから認証後にコールバックハンドリングエンドポイントが提供されています。
認証サービスのコールバック URL には以下のフォーマットに従った値を設定してください。
https://account.{region}.gen2.gs2io.com/{ownerId}/{namespaceName}/type/{type}/callback
https://account.ap-northeast-1.gen2.gs2io.com/aAbBcCdD-project/namespace-0001/type/0/callback
認証のコールバックを受け取ると以下のURLに遷移します。
https://account.{region}.gen2.gs2io.com/{ownerId}/{namespaceName}/type/{type}/done?id_token={idToken}
URLのクエリストリングに認証して得られたIDトークンが渡ってきます。
Firebase Authentication のような GS2 以外の認証機能の利用
IDトークンさえ手に入ればその手段は上記手順である必要はありません。
認証サービス以外からの登録を拒否
マスターデータで OpenID Connect 連携が設定されたスロットは任意のユーザー識別子とパスワードを使用した引き継ぎ情報の登録および、引き継ぎの実行は行えなくなります。
実装例
匿名アカウントの作成
GS2-Account はネームスペースという階層をもち、1つのプロジェクト内に複数のアカウントプールを持つことができます。 ネームスペースの用途は特に定めていません、配信地域ごとにネームスペースを分けるなど必要に応じて活用することができます。
var result = await gs2.Account.Namespace(
namespaceName: "namespace-0001"
).CreateAsync();
var item = await result.ModelAsync();
var userId = item.UserId;
var password = item.Password;
const auto NamespaceName = "namespace-0001";
const auto Future = Gs2->Account->Namespace(
NamespaceName
)->Create();
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;
const auto Result = Future->GetTask().Result();
const auto Future2 = Result.Model();
Future2->StartSynchronousTask();
if (Future2->GetTask().IsError()) return false;
const auto Result2 = Future2->GetTask().Result();
var UserId = Result2.UserId;
var Password = Result2.Password;
匿名アカウントを使用したログイン
GS2-Account の認証処理は、一定期間でやりなおす必要があります。 そのような処理を自動的に行ってくれるユーティリティクラスである Gs2AccountAuthenticator を使用したログイン例を示します。
GS2 のAPIクライアントに対して、Gs2AccountAuthenticatorと作成した匿名アカウントのユーザーID、パスワードを指定することでログイン中のセッション情報を表す GameSession インスタンスを取得できます。 以降、ログイン中のプレイヤーの情報にアクセスするにはこの GameSession インスタンスを使用することになります。
var gameSession = await gs2.LoginAsync(
new Gs2AccountAuthenticator(
accountSetting: new AccountSetting {
accountNamespaceName = this.accountNamespaceName,
}
),
account.UserId,
account.Password
);
const auto NamespaceName = "namespace-0001";
const auto KeyId = "grn:gs2:{region}:{yourOwnerId}:key:namespace-0001:key:key-0001";
const auto Future = Profile->Login(
MakeShareable<Gs2::UE5::Util::IAuthenticator>(
new Gs2::UE5::Util::FGs2AccountAuthenticator(
NamespaceName,
KeyId
)
),
UserId,
Password
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;
const auto Result = Future->GetTask().Result();
引き継ぎ情報の登録
スロット番号 に異なる値を指定することで、1つのアカウントに対して複数の 引き継ぎ設定 を保持できます。
たとえば、 スロット番号:0 にメールアドレス・パスワード を、 スロット番号:1 にソーシャルメディアのID情報を格納するようにし、 ゲームプレイヤーは好みの引き継ぎ手段を選択できるようにする といった運用が可能です。
var result = await gs2.Account.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).TakeOver(
type: 0
).AddTakeOverSettingAsync(
userIdentifier: "user-0001@gs2.io",
password: "password-0001"
);
const auto NamespaceName = "namespace-0001";
const auto Type = 0;
const auto UserIdentifier = "user-0001@gs2.io";
const auto Password = "password-0001";
const auto Future = Gs2->Account->Namespace(
NamespaceName
)->Me(
AccessToken
)->TakeOver(
Type
)->AddTakeOverSettingAsync(
UserIdentifier,
Password
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;
const auto Result = Future->GetTask().Result();
登録済みの引き継ぎ情報一覧取得
var items = await gs2.Account.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).TakeOversAsync(
).ToListAsync();
const auto NamespaceName = "namespace-0001";
const auto It = Gs2->Account->Namespace(
NamespaceName
)->Me(
AccessToken
)->TakeOvers();
TArray<Gs2::UE5::Account::Model::FEzTakeOverPtr> Result;
for (auto Item : *It)
{
if (Item.IsError())
{
return false;
}
Result.Add(Item.Current());
}
引き継ぎの実行
string userId;
string password;
try {
var result = await gs2.Account.Namespace(
namespaceName: "namespace-0001"
).DoTakeOverAsync(
type: 0,
userIdentifier: "user-0001@gs2.io",
password: "password-0001"
);
var item = await result.ModelAsync();
userId = item.UserId;
password = item.Password;
} catch(Gs2.Gs2Account.Exception.PasswordIncorrect e) {
// Incorrect password specified.
}
const auto NamespaceName = "namespace-0001";
const auto Type = 0;
const auto UserIdentifier = "user-0001@gs2.io";
const auto Password = "password-0001";
const auto Future = Gs2->Account->Namespace(
NamespaceName
)->DoTakeOver(
Type,
UserIdentifier,
Password
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError())
{
auto e = Future->GetTask().Error();
if (e->IsChildOf(Gs2::Account::Error::FPasswordIncorrectError::Class))
{
// Incorrect password specified.
}
return false;
}
// obtain changed values / result values
const auto Future2 = Future->GetTask().Result()->Model();
Future2->StartSynchronousTask();
if (Future2->GetTask().IsError()) return false;
const auto Result = Future2->GetTask().Result();
OpenID Connect の認証処理
以下のサンプルのアプリ内ブラウザには unity-webview を使用しています。
public static async UniTask<string> OpenAuthentication(
WebViewObject webView,
Gs2Domain gs2,
string namespaceName,
IGameSession gameSession,
int type
) {
string idToken = null;
webView.Init(
separated: true,
ld: url =>
{
if (new Uri(url).LocalPath.EndsWith("/done")) {
var codeField = new Uri(url).Query.Replace("?", "").Split("&").Select(v => new KeyValuePair<string,string>(v[..v.IndexOf("=", StringComparison.Ordinal)], v[(v.IndexOf("=", StringComparison.Ordinal)+1)..])).FirstOrDefault(v => v.Key == "id_token");
idToken = Uri.UnescapeDataString(codeField.Value);
webView.SetVisibility(false);
}
}
);
webView.LoadURL(
(await gs2.Account.Namespace(
namespaceName
).Me(
gameSession
).GetAuthorizationUrlAsync(
type
)).AuthorizationUrl
);
webView.SetInteractionEnabled(true);
webView.SetVisibility(true);
await UniTask.WaitWhile(() => idToken == null);
return idToken;
}
OpenID Connect を使用した引き継ぎ情報の登録
var result = await gs2.Account.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).TakeOver(
type: 0
).AddTakeOverSettingOpenIdConnectAsync(
idToken: "id-token"
);
const auto NamespaceName = "namespace-0001";
const auto Type = 0;
const auto UserIdentifier = "user-0001@gs2.io";
const auto Password = "password-0001";
const auto Future = Gs2->Account->Namespace(
NamespaceName
)->Me(
AccessToken
)->TakeOver(
Type
)->AddTakeOverSettingOpenIdConnect(
"id-token"
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;
const auto Result = Future->GetTask().Result();
OpenID Connect を使用した引き継ぎの実行
var result = await gs2.Account.Namespace(
namespaceName: "namespace-0001"
).DoTakeOverOpenIdConnectAsync(
type: 0,
idToken: "id-token"
);
var item = await result.ModelAsync();
var userId = item.UserId;
var password = item.Password;
const auto NamespaceName = "namespace-0001";
const auto Type = 0;
const auto UserIdentifier = "user-0001@gs2.io";
const auto Password = "password-0001";
const auto Future = Gs2->Account->Namespace(
NamespaceName
)->DoTakeOverOpenIdConnect(
Type,
"id-token"
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError())
{
return false;
}
// obtain changed values / result values
const auto Future2 = Future->GetTask().Result()->Model();
Future2->StartSynchronousTask();
if (Future2->GetTask().IsError()) return false;
const auto Result = Future2->GetTask().Result();
その他の機能
プレイヤーごとの時刻オフセット設定
Game Server Services のアカウント情報には時刻のオフセットを持たせることが可能です。 このオフセットには未来に向けて、プレイヤーが何分間先の状態として振る舞うかを設定することが可能です。
この機能を利用すると、本番環境内でもQA担当者は1時間未来の状態でゲームを遊ばせるようなことを実現できます。 こうすることで、イベントの開始時刻になってもイベントがオープンされないといった問題にいち早く気づくことができます。
アカウントの利用停止
ゲーム内での素行が悪いプレイヤーを利用停止にすることが可能です。