simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

CubeWalk:ワールドチケットの仕組み

実装メモです
整理出来たら消します。

ワールドチケット?

世界を編集できる権限をユーザーに付与したいという要求に対して、出した答え

ユーザーのインベントリに ticket カタログに所属するワールドチケットアイテムを買ってもらい
そのチケットには有効期限が設定されていて、購入から時間が経過すると消えていくしくみ

スタックできるので 2個買うと、1チケット消費され、さらに有効期限が続くという考え方から始まります。

実装試験してみると、購入からの有効期限となっており、2チケット買っても、消費される瞬間はほぼ同じ
残念ながらスタックすることはできないため、出端から計画は崩れました。

既存のキューブとのバッティング

PlayFab のアイテムはカタログに所属しています。
いままでは cube カタログしか使ってきませんでした。

チケットは ticket カタログ
キューブは cube カタログ

にそれぞれ所属するとします。

カタログを分けて扱うようにするので、cube カタログのものと ID がバッティングするものを作らない限りは影響なしと思いたい
既存の各種クライアントコードにてカタログバージョンチェックを入れる必要がありそうだ

実際 ID 重複が許されるのかも見ると→重複は可能でした

ので、ID決定で困ることはないですが、クライアントのシステムにてカタログ所属考慮が抜けているとロジック破壊が起きるので
インベントリの情報を扱っているところはすべてカタログで分岐を書かなくてはならない 書いてなかったら直さなければならないことになりました。

準備として修正を進めます。→修正完了

いきなり編集権利を買うのではなく、交換チケットを買ってほしいときに権利と交換する案

チケットの種類は worldチケット 1つ、買うときにカウント消費アイテムとして、永続化して保持してもらう
新たに、ワールドごとの編集権利アイテムというものを購入不可能なものとして、world チケットと交換可能で作る
ワールド編集時に権利アイテムが無ければチケットと交換、これをユーザーに確認してサーバー処理で安全に交換する

良い点:
チケット購入時にワールドを確認する必要がなくなる
買ってすぐに使わなくても価値は下がらない
編集開始時にチケット交換をたずねるだけになれる
権利の重複フローをシステムで消せる

どうやって購入?

ストアという概念を定義し、システムメニューから選べるようにします
できれば、ユーザー特定情報がユーザーから申告できるように、プロフィール画面を作る→名前で検索できるのでしばらくはいらない

ストアについて記事を探して実装イメージを固めてみます

ストアとリアルマネー

なるほどストア機能は仮想通貨による価格設定などが良さそうですね。
ただ、仮想通貨とリアルマネーのトレードをして管理するようになると、仮想通貨でゲーム内でいろいろなアイテムを購入できるものにした場合
これは日本国では資金決済法の適用が義務付けられるので、例えば同人誌即売のようにアイテムを一意に決めて売るとは勝手が違うため、事業者登録が必要になります。
なので、このゲームは複雑にならないよう、仮想通貨によるアイテムの販売、そのための仮想通貨の購入は避けます。

そこで、直接ワールド編集権利のみを、このゲームのサーバー運営代として支払ってもらう仕組みにします。
権利だけを得るチケットをリアルマネーと交換するだけ
simplestar-tech.hatenablog.com
こちらの記事が参考になりそう。

メニューにストアを作り、そこから購入したチケット一覧を並べて、所持数を表示し
チケットと枚数を指定したら購入 url が開き、購入後に購入しましたボタンを押したら
成功→チケット数が増える
のフローを経て、ストア画面が更新表示される

という UI とロジック動くところを作ってみます。

ショップUIを新規作成

現在は Esc キーでシステムメニューが出る 今はそこからフライトカメラモードとゲーム終了・再開を押せる状況
このメニューにタブ切り替え機能を用意して、そこで新しいコンテンツビューを表示できるようにします

アイテムの表示

やり方としては catalog と store の両方の情報を取り出し、インベントリと合わせて
各種アイテム情報と所持数、購入ボタンを配置することになる

所持数は…購入画面にて + ボタンを押すと、所持数+ いくつ という表示で行おうと思います。
ひとまずショップ内容の表示まで手を動かしました。


アイテム購入画面

新しくダイアログを表示して、既存の所持数 + ボタンで購入するアイテム数を制御し
そのうえで購入ボタンを押すと、アイテム数が増える

なんと、キューブアイテムは 2 個で 1キューブなので、価格を倍額で表示しておこう

現在の所持金も表示しなければならないことにも作っていて気が付けました

f:id:simplestar_tech:20200724222139p:plain

アイテムの購入ボタンを押すと、所持金、個数、可能な購入個数などの制御が正しく動くところまでできました。
あとはキックされた処理でアイテム購入を行う部分を実装することになりそうです。

仮想通貨でのアイテムの購入処理

リアルマネーとは異なり、こちらは簡易な手順なので、試験することろまで進めます。

実はアイテムは個数指定で購入することができないのが PlayFab の作りでした。
ワークアラウンドとしては、アイテム数を増やしたりして、サーバー側でうまいこと仮想通貨を減らしてほしいとのこと

これは AzureFunction を増やして対処する

同じように、現実のお金の場合もアイテムは一つずつしか購入できないのか調査してみます。

PlayFabClientAPI.StartPurchase

リクエストに Quantity を指定できるのでうまくいっている様子
しかし?

RM 以外の仮想通貨も使えるのだろうか?

はい、その通り!

仮想通貨での動作を確認したときの記録がこちら

スロットを持たない新規アイテムの購入フロー

アイテムのうち、インベントリのスロット番号を持つものがありますが
新規購入をするとこの値が設定されていないので、いつ設定するか考える必要があります。

わざと所持していないアイテムを買ってみて動作をみてみましょう。

この考えは正しく、現在スロット操作が不能になる
理由 inventory item has no custom data slot

アプリの処理として、これを生成しているコードは AzureFunction
処理としては既存のインベントリから取り出したアイテムが Slot 指定を持たないことに疑問を持ち、不正リクエストとして返している
ただ、それは不正なリクエストではなく、サーバー側の不正状態といえる

今後、スロットに所属しないアイテムを許可することを考えると、ロジックを見直してみる必要がある
無視してもよさそうなので、無視

ここでスロットを持たないアイテムにスロットを追加するのはやめた。
逆にリクエストに問題が無ければ受け付ける仕組みとなっていたため、インベントリ情報をローカルで表示する際、アイテムにスロット番号が指定されていない場合は
ローカルにてスロット番号をリクエストすることで解決できるゴールが計算できる

この計算が正しいか確認する

残念ながら、クライアントからのリクエストは受け付けられなかった。
カスタムデータを持たないアイテムに対するスロット指定はもともと受け付けないようにしていたからである。

ただ、これからはスロット指定を持っていないアイテムについてもスロットを指定できるようにするため、この問題は既存ロジックの書き換えで対処できることになる
修正後

破壊的変更なのでメンテナンスして調べる
そろそろ staging 環境など用意した方が良さそう

アイテムは置けるので、スロットの反映は許可されることになった
f:id:simplestar_tech:20200725133958p:plain

スロットをはじめから持たないキューブが得られた時、これをクラアントから空いている箇所に設定したはずが
どういうわけか重複するエラーを作ることができた。
あとから設定されたものでスロットが上書きされるため、ローカルの見た目はアイテムが消えたように見える

再現しなくなってしまったが
サーバー側操作でわざとスロットを重複して与えることができるので、その場合に空いている場所へ退避してからリクエストするクライアントになるよう処理をくわえることにした。

動作に問題はない

リアルマネーによるアイテムの購入

仮想通貨に関しては現実資金の決済なしにアイテムは仮想通貨によって購入することができました。
実際のお金を必要とする場合は、確か、購入用の Web ページを表示するところからです。

実際に資金の移動と購入が行えるかチェックしてみます。

行えました。
返金は 180 日以内なら可能なので、試験的にお金を使ってもすべて取引は無かったことにできる様子
少しテストの障壁が低くなった

PayPal を選んでみたが、手数料は約 13% ほどとられるみたい

権利アイテムの定義

お金で買ったのはチケットなので、これは回数によって消費する券

これとは別で、編集したいワールドにて編集操作を行ったら、サーバー側で権利アイテムを持っているかチェックし
これを持たない場合にチケットを利用するか聞くフローが作られ
もしチケットも持っていない場合はチケット購入画面を開くフローが作られる

というのをやります。

最初はワールドごとの権利アイテムを作り、続いて、サーバー側で編集したいワールドの権利アイテムを所持しているかを見つける処理から進めてみましょう。

world カタログの、リクエストされている worldName と一致する ItemId のアイテムをインベントリに持っている場合は、権利アリ
そうでない場合は権利なしを返すことで作成できました。

権利なしを受け取ったら、権利購入へ進める

キューブ操作の戻り値を正しく処理することを今まで行っていないはずなので、その部分を見ていく
この点を確認すると、権利を持たない場合の戻り値を正しく受け止めることができるようになるだろう

加えて、ALB (Amazon のアプリケーションロードバランサー)の拒否戻り値を使って、ユーザーにメンテ中のサーバーであることを知らせるなど
はじめて、ユーザに理由を添えて示すことができるようになるはずである

できますね。
ステータスコードで分離してみます。

キューブ編集中の状態でエラーが起きるので、操作不能にしてからメッセージを表示し、閉じたら元に戻すようにしたいところ

操作不能、復帰処理

入力ステート管理があり、このステートを切り替えるで完了
今までシステム操作ステートに入ったらシステムメニューが表示されるような仕組みだったが、明示的に Esc キーを押したときだけ表示に切り替えた

権利がないときの UI

新しい独立パネルを作り、現在の保有チケット数を表示
権利を取得したい、チケットを使いたいワールド名を示して、一日編集権に交換することを確認する

確認できたら、編集権を得た画面になり、パネルと閉じた後は 1日だけ利用できるようになる
アイテムは勝手に 1日経つと消費される

新規実装ですね、パネルから作っていきます。

持っているチケットアイテムが列挙されるパネルが、編集権を持たないワールドを編集したときに現れるようになりました。(試験目的で同じチケットを3つ並べてます)
f:id:simplestar_tech:20200726220433p:plain

チケットアイテムを一つも持たない場合は、ショップへ移動するフローが構築されることになってます。
f:id:simplestar_tech:20200726220827p:plain

チケット購入ページへの遷移

無いので作ってみます。

UI操作をコード化して、ショップのチケットカタログを開くところまでは作れました。

チケットを消費してワールド編集権利を得る

バンドル化するのも良いと思いましたが、チケットとワールドごとの権利は 1対多なので
新しく AzureFunction を作ろうと思います。

構想したものが連休中に終らず…タイムアップです

これは新しい Azure Function ですね

ゴリゴリ実装してコード整理、動作確認完了!

決済操作した後、オーダー番号で確認フローを進めない限り、クラアントからの支払いは完了しないことがわかった。

あとは権利を手にしたときに、サーバーが眠ってたら起こす仕組みを作れば、課金まわりの一通りの機能がそろう

バックアップ時に残り日数を減らし 0 になったら寝る

残り日数とは?というものですね。
概念というよりは、ワールドデータサーバーごとに参照、変更できるゲーム内で一意の値のことですね。

一つのキーに複数のワールドデータを詰めると、競合で消えてしまうデータができてしまうので
一つのキーに一つのワールドの活動限界日数を記録することにします。


000 : 1

Set で null 文字を渡すと消せることから、バックアップで 1 → 0
0 → null としてカウントダウンした値を Set する仕組みにし、 null のときはバックアップしない(ワールド寝てるから)

ユーザーが編集権利を購入したとき、現在のワールドチケットの有効稼働日数を Set する
セット前に null であることを確認したら、ワールドを呼び覚ます AzureFunction を実行する

で、いけると思っている

作ってみます。。。。

ワールドを寝かしたり、呼び起こす AzureFunction はできた。
これを他の AzureFunction 実行時にも呼び出せるか調べて、書き換えてみます。

タイトルデータ internal が PlayFab にはあってそうですが、その特性を調べていきます。

最大で 15分ほど遅れがあるそうですが、全然大丈夫
一日ごとのバックアップ時に 1ずつ減らして 0 → null のときに、サーバーを眠らせるようにして、動作確認とれました。

権利購入成功時に、サーバーが寝てたら起こす

権利購入時には TitleInternalData で、ワールドの稼働日数を確認し null だったときに
start インスタンスでサーバーを起こす

その後 TitleInternalData にはワールド名をキーにチケットの日数を与える

動きました!!
ワールドチケットの仕組み、最初に思い描いた仕様を形にすることができました。

とまぁ、ゲームの一機能を作るって、だいたいこんな感じですね。
めっちゃつまらない&苦痛の連続でした