ギルド機能の提供を開始しました
はじめに
ゲームにはプレイヤー同士が長期の協力プレイをするための仕組みとして、ギルドやクランという仕組みが導入されるケースがあります。
GS2 にはこれまでこのような仕組みを実現する方法がありませんでした。
追加された機能の詳細
新しく追加された GS2-Guild はギルドメンバーの管理と、ギルドに関する情報を紐づけるためのギルドユーザーの仕組みを提供します。
ギルドユーザー
GS2-Guild では、ギルドレベルやギルドメンバーで共有するインベントリを実現するために「ギルドユーザー」という仕組みを持ちます。
ギルドユーザーは GS2 のいちアカウントとして処理され、全ての GS2 のマイクロサービスを使ってこのギルドユーザーに紐づくプロパティを管理できます。
従来のGS2-Accountのユーザーはパスワードを使ってログインしますが、ギルドユーザーはパスワードではログインできません。
その代わり、GS2-Guild にユーザーフェデレーションの仕組みが用意されています。
ユーザーフェデレーションは、ギルドメンバーのアクセストークンを用いて、ギルドユーザーのアクセストークンを取得する仕組みです。
この仕組みによって、ギルドメンバーはギルドユーザーのアクセストークンを取得することで、ギルドユーザーに紐づくレベルを取得したり、インベントリの操作が可能となります。
ロール
次に、ギルドの特徴としてギルドメンバーごとに権限管理できるようになっていることです。
具体的にはギルドマスターのようなギルドを管理する立場のプレイヤー向けの権限と、ギルドメンバー向けのギルドで活動はするが管理する権限は持たないようなプレイヤー向けの権限を設定できます。
ロールの権限の内容はゲーム開発者自由に定義することができ、さらにプレイヤーが自由にロールを作成できるカスタムロールの仕組みも有しています。
ユーザーフェデレーションをする際にメンバーに割り当てられたロールに基づく権限でギルドユーザーとして実行できるAPIを絞り込むことができるようになっています。
ポリシー
権限は GS2-Identifier のポリシードキュメントフォーマットを用いて定義します。
具体的な例として、ギルドの管理権限とギルドユーザーの GS2-Experience のAPIを呼び出す権限を付与するとしましょう。
{ "Version": "2016-04-01", "Statements": [ { "Effect": "Allow", "Actions": [ "Gs2Guild:Describe*", "Gs2Guild:Get*", "Gs2Guild:AcceptRequest", "Gs2Guild:RejectRequest", "Gs2Guild:DeleteMember", "Gs2Gateway:SetUserId", "Gs2Experience:*" ], "Resources": ["*"] } ] }
前半の 「Gs2Guild」で始まる部分がギルドユーザーとして実行可能なAPIを宣言する部分で、メンバー管理に必要なAPIが指定されています。
「Gs2Gateway:SetUserId」にはギルドユーザーに関する通知を受け取れるようにする権限設定です。
この宣言がないと通知を受け取ることができず、ギルドへの参加申請が届いたことや、ギルドユーザーに関するプロパティの変化をアプリが知ることができなくなります。
最後の「Gs2Experience:*」が GS2-Experience の全てのAPIを呼び出す権限を付与する部分です。
さて、ここで 「Gs2Experience:*」と書いてしまうと、「Gs2Experience:AddExperienceByUserId」 のような任意のプレイヤーに任意の経験値を付与するAPIを呼び出せるようになるのでは?と不安に思うかもしれません。
しかし、ユーザーフェデレーションをする場合、フェデレーションする前のアクセストークンが持つ権限より強い権限を得ることはできません。
そのため、一般的なゲームプレイヤーが持つであろう「ApplicationAccess」権限では元々「Gs2Experience:AddExperienceByUserId」を呼び出すことはできませんので、ロールのポリシードキュメントに全てのAPIを呼び出せるように記述したとしても、「Gs2Experience:AddExperienceByUserId」を呼び出せるようにはなりません。
ちなみに、余談として申請の承認やギルドメンバーの除籍APIを呼び出せない、一般的なギルドメンバーの権限は以下のように設定します。
{ "Version": "2016-04-01", "Statements": [ { "Effect": "Allow", "Actions": [ "Gs2Guild:Describe*", "Gs2Guild:Get*", "Gs2Gateway:SetUserId", "Gs2Experience:*" ], "Resources": ["*"] } ] }
GS2-Showcase でギルドマスターだけがギルドユーザーとして商品を買えるようにしたければ、ギルドマスターのロールにだけ「Gs2Showcase:Buy」をつけるといった対応をすることで細やかな権限管理ができます。
参加承認
権限管理のセクションで軽く触れましたが、GS2-Guild のギルドには参加するために既存のギルドメンバーから承認を得る必要があります。
ただし、ギルド作成時に「参加方針」を「自由参加」に設定することで、ギルドに参加申請を出すと直ちにギルドに参加することができます。
参加人数の上限
GS2-Guild には参加人数の上限を設定できます。
この参加人数はギルドごとに個別に引き上げることができます。
この仕組みを使うことで、ギルドレベルが上がることで参加可能なプレイヤー数を増やすような仕様を実現できます。
ギルドの検索
プレイヤーがギルドを見つけるための仕組みとして、ギルドの検索機能があります。
検索条件には主に4つの要素を指定できます。
表示名
ギルドの表示名の部分一致で絞り込むことができます。
参加方針
ギルドの参加方針が「自由参加」か「承認制」かで絞り込むことができます。
属性値
ギルドには最大5個の属性値を設定できます。
属性値は整数値で、検索条件には完全一致の条件を最大10種 OR で指定できます。
「ガチプレイ」「まったりプレイ」のようなギルドの運営方針や、ゲーム内の「所属国家」などで絞り込むことを想定しています。
参加人数
最後の条件はギルドの参加人数が上限に達しているギルドを除外することができます。
プレイヤーは原則新しく参加するためのギルドを探すために検索をしているでしょうから、満員のギルドは除外する方が合理的かもしれません。
一方で、プレイヤーが表示名で絞り込んでいる場合は友達の所属しているギルドを探しているのかもしれません。その時検索結果にまったく表示されないとプレイヤーは困惑してしまう可能性があります。
UXを考慮しつつこの条件を指定できるよう、検索のパラメーターとしてこの値を指定できるようにしています。
実装例
ギルドを作成
var result = await gs2.Guild.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).CreateGuildAsync(
guildModelName: "guild-model-0001",
displayName: "My Guild",
joinPolicy: "anybody",
attribute1: 1,
attribute2: null,
attribute3: null,
attribute4: null,
attribute5: null,
customRoles: null,
guildMemberDefaultRole: null
);
var item = await result.ModelAsync();
ギルドを検索
var items = await gs2.Guild.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).SearchGuildsAsync(
guildModelName: "guild-model-0001",
displayName: "My Guild",
attributes1: new int[] { 0, 1 },
attributes2: null,
attributes3: null,
attributes4: null,
attributes5: null,
joinPolicies: null,
includeFullMembersGuild = null
).ToListAsync();
ギルドに参加リクエストを送信
var result = await gs2.Guild.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).SendRequestAsync(
guildModelName: "guild-0002",
targetGuildName: "guild-0002"
);
送信した参加リクエストの一覧を取得
var items = await gs2.Guild.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).SendRequestsAsync(
guildModelName: "guild-0002"
).ToListAsync();
参加リクエストを取り下げ
var result = await gs2.Guild.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).CancelRequestAsync(
guildModelName: "guild-0002",
targetGuildName: "guild-0002"
);
ギルドユーザーとしてアクセスするためのゲームセッションを取得
var guildGameSession = await gs2.Guild.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).AssumeAsync(
guildModelName: "guild-model-0001",
guildName: "guild-0001"
);
受信した参加リクエストの一覧を取得
var domain = gs2.Guild.Namespace(
namespaceName: "namespace-0001"
).GuildGameSession(
guildModelName: "guild-0001",
guildGameSession: guildGameSession
);
var items = await domain.ReceiveRequestsAsync(
).ToListAsync();
参加リクエストを承認
var result = await gs2.Guild.Namespace(
namespaceName: "namespace-0001"
)->GuildGameSession(
"guild-0001", // guildModelName
GuildGameSession // guildGameSession
).ReceiveMemberRequest(
fromUserId: "user-0001"
).AcceptRequestAsync(
);
参加リクエストを否認
var result = await gs2.Guild.Namespace(
namespaceName: "namespace-0001"
)->GuildGameSession(
"guild-0001", // guildModelName
GuildGameSession // guildGameSession
).ReceiveMemberRequest(
fromUserId: "user-0001"
).RejectRequestAsync(
);