simplestarの技術ブログ

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

Unity: BehaviorTree 勉強2

アニメーションする Task は Running しか返さないというのはどうか?

止める条件があるのなら、その親に Conditional Evaluator を置き、これが失敗を返したら、子であるアニメーション Task は中断されるため

参考

www.slideshare.net


目的地が設定されているか?
yes→目的地へ向かう
no→次の行動を選択

selector

sequence

  • hasDestination
  • gotoDestination

sequence

  • nextAction

結局アクションを定義してから組み合わせになるのか

ランダムに目的地を決める
プレイヤーを見つけているか?
プレイヤーの方向の目的地を決める
目的地は決まったか?
目的地に向かって歩く
目的地に着いたか?
十分プレイヤーに近づいたか?
プレイヤーの方向を向いているか?
プレイヤーの方向を向く
座り込む
座り続ける
立ち上がる


うーん、一連のながれが sequence で、その一連のながれが通らないときに selector で別の sequence へ流れるかんじ?

最初は
十分プレイヤーに近づいたか?→yes→プレイヤーの方向を向く→十分プレイヤーに近づいたか?→yes→座り込む→十分プレイヤーに近づいたか?→yes→プレイヤーの方向を向いているか?→yes→座り続ける
目的地に着いたか?→yes→
目的地は決まったか?→yes→目的地に向かって歩く
プレイヤーを見つけているか?→yes→プレイヤーの方向の目的地を決める

え、ループできるのか
ループってどうやってツリー表現

考え中

なるほど、継続しつづけるアクション 待機モーションなどを running し続けるで良さそう
これが最終的な安置として

どうやって抜け出すの?

それが前回勉強したなにかの conditional なんちゃら

Selector Evaluator

実行中を返す Action が子タスクの優先度の低いものであったなら、それより高い優先度の Action の評価をもう一度行う

これもそうだけど、もっと self とか low priority とかでもできたような?

Sequence NodeのAbort TypeをNoneからSelfに変更すると、子ノードの running 中の左側を毎回評価して、failure なら runnning を中断できる
つまり、抜け出せる AbortType self で

または、同階層の右側が running でも Abort Type Lower Priority でも

なるほど Abort の目的語が Lower Priority で右がわ、Self が自身の子ノードという意味だったのか

やっと解読できてきた
つまり

Unity: Behavior Designer のノードの勉強

基本的なことはこちら
simplestar-tech.hatenablog.com

ビヘイビアツリーを構成しているノードは三つに分けられる、それぞれ Task, Composite, Decorator である。

・Task はツリーのリーフ要素で、1フレームに許された計算時間で条件判定やアクションを実行し、戻り値として1)Running(まだタスクが残っている), 2)Failure(条件に合わない、または失敗), 3)Success(条件一致、または成功) の三つのいずれかを返す。
・Composite は子ノードの実行を制御するノードで、シーケンスとセレクターの二種類が存在する。
シーケンスは最初の子ノードが実行完了するまで待機し、成功を返した場合に、次の子ノードを実行し、この操作を繰し、最後の子ノードが成功を返したら、シーケンスノードも成功を返す。一度でも子ノードが失敗を返したら、残りの子ノードは無視して失敗を返す。子ノードが実行中を返すようなら、シーケンスも実行中を返す。
セレクターは優先度順に子ノードの選択を行い、選択したノードを実行する。シーケンスと違う点は、どれか一つの子ノードが成功を返した時点で、まだ実行していない子ノードがあっても、成功を返す点である。
セレクターには指定した割合の確率で選択する Probability Selector や、1フレームにすべての子ノードを実行する Parallel がある。
・Decorator ノードは特質系で、子ノードを一つしかとることができない。ノードのタイプによって振る舞いはさまざまで、たとえば n 回子ノードを繰り返し実行してから成功を返したり、子ノードがn秒以上実行中だったら成功失敗に関わらず強制的に失敗を返したりする。

これから Behavior Tree を活用していくので、使い方に慣れるため、各ノードの動きを確認しながら、道具として使えるようになるため、覚えていこうと思います。

## Parallel

子タスクを一斉に並列実行し、いずれかが失敗を返したらすべての子タスクを停止して失敗を返す
実行中の子タスクが残るなら、実行中を返し
すべての子タスクが成功したとき、成功を返す
イメージとしては並列実行の And 条件 if 文って感じですかね

f:id:simplestar_tech:20210703130241p:plain
Parallel

## Parallel Complete

子タスクを一斉に並列実行し、最も早く答えを出した子タスクの答えを返す
全部成功していて、残る実行中のものがあったとしても、最初に成功を返した子タスクを見つけた時点で成功を返す
イメージとしては並列実行の Or 条件 if 文って感じですかね、False を返すのが先だと False が返るところが全然違う

f:id:simplestar_tech:20210703131331p:plain
Parallel Complete

## Parallel Selector

子タスクを一斉に並列実行し、すべて失敗となるまで待つが、いずれかが成功を返した瞬間に子タスクをすべて停止して成功を返す
イメージとしては並列実行の Or 条件 if 文って感じですね
f:id:simplestar_tech:20210703131954p:plain

## Priority Selector

float GetPriority(); を Action Task 側で実装して返す必要がある
この値が大きい順で子タスクを順番に実行して、先に成功を返したタスクが生まれたら成功を返すもの

f:id:simplestar_tech:20210703142958p:plain
Task class

## Random Selector

ランダムに子タスクの順番を決めて Selector として、成功を見つけるまで続け、成功を返す子タスクによって、続く子タスクの実行を停止して、成功を返す
イメージとしては順番実行の Or 条件 if 文って感じで、評価順がランダムというものですね

f:id:simplestar_tech:20210703143258p:plain
Random Selector

## Random Sequence

ランダムに子タスクの順番を決めて Sequence として、すべてが成功するまで続け、失敗を返す子タスクによって、中断して子タスクの実行を停止して、失敗を返す
最後の子タスクが成功したときにやっと成功を返すという
イメージとしては順番実行の And 条件 if 文って感じで、評価順がランダムというものですね

f:id:simplestar_tech:20210703150525p:plain
Random Sequence

## Selector

いずれかが成功したら成功を返し、残りの実行を中断する
イメージとしては順番実行の Or 条件 if 文って感じ

f:id:simplestar_tech:20210703150847p:plain
Selector

## Selector Evaluator

実行中を返す Action が子タスクの優先度の低いものであったなら、それより高い優先度の Action の評価をもう一度行う様子
実行中のタスクより低い子タスクの実行はせず
優先度が高いタスクは常にチェックしたいといったギミックに良いのかも たとえばプレイヤーを視認していること という条件に使って、続くアクションに追いかけるといった running ステータスを返す何かが挟まるなど
イメージとしては順番実行の Or 条件 if 文って感じだが、どういうわけかさっき調べた条件をもう一度しらべいにく
いくつもの Task のうち、最初に成功を返した Task の結果をもって成功を返す

f:id:simplestar_tech:20210703151759p:plain
Selector Evaluator

## Sequence

順次実行して、失敗が返ると即子タスクを停止して失敗を返す
イメージとしては順番実行の And 条件 if 文って感じ

f:id:simplestar_tech:20210703153116p:plain
Sequence

## Utility Selector

float GetUtility の値が大きいものを常に選んで実行する
子タスクすべてを見ていて、Utility 値が実行中に小さくなったなら、他の Utility 値が大きいタスクの実行へと移る
いずれかが成功を返したら、他のタスクを停止して成功を返す でも、いずれの子タスクも Running を返すようにする使い方がメインなのかな
Priority Selector との違いは、それまで最高Utility だったタスクが Running 中でも、他の Utility が高まったら中断するところかな

f:id:simplestar_tech:20210703153501p:plain
GetUtility
f:id:simplestar_tech:20210703154804p:plain
Utility Selector

Unity:非VRからVRモード切替機能とその逆切り替えをするScript記述方法

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.Management;

namespace CubeWalk
{
    /// <summary>
    /// VRモード切替器
    /// </summary>
    public class VRSwitcher : MonoBehaviour
    {
        /// <summary>
        /// 非VR時にのみアクティブになるオブジェクト群
        /// </summary>
        [SerializeField] List<GameObject> keyMouseObjects;
        /// <summary>
        /// VR時にアクティブになるオブジェクト群
        /// </summary>
        [SerializeField] List<GameObject> vrObjects;
        /// <summary>
        /// VR アセット位置調整用
        /// </summary>
        [SerializeField] Transform vrAssets;
        /// <summary>
        /// プレイヤーオブジェクト発見用
        /// </summary>
        [SerializeField] Transform players;

        public void EnableVR()
        {
            StartCoroutine(this.CoEnableVR());
        }

        public void Enable2D()
        {
            XRGeneralSettings.Instance.Manager.StopSubsystems();
            XRGeneralSettings.Instance.Manager.DeinitializeLoader();
            foreach (var item in this.vrObjects)
            {
                item.SetActive(false);
            }
            foreach (var item in this.keyMouseObjects)
            {
                item.SetActive(true);
            }
        }
        
        IEnumerator CoEnableVR()
        {
            yield return XRGeneralSettings.Instance.Manager.InitializeLoader();
            while (!XRGeneralSettings.Instance.Manager.isInitializationComplete)
            {
                yield return null;
            }
            XRGeneralSettings.Instance.Manager.StartSubsystems();

            // VR開始位置をプレイヤー位置に調整
            foreach (Transform vrm in this.players)
            {
                if("Player" == vrm.tag)
                {
                    this.vrAssets.position = vrm.position;
                    break;
                }
            }

            foreach (var item in this.keyMouseObjects)
            {
                item.SetActive(false);
            }
            foreach (var item in this.vrObjects)
            {
                item.SetActive(true);
            }
        }
    }
}

CubeArtWorld:爆発するキューブと四散する仕組み

爆発すると周囲のキューブをごっそり削り取り、削り取られたキューブが四散するというものが可能なのか調べていきます

f:id:simplestar_tech:20210319173631p:plain
ラクガキキングダムのインポートできます

できましたー

CubeArtWorld:過去へさかのぼるタイムトラベル

f:id:simplestar_tech:20210223170337p:plain
過去の世界とは

実はこの半年、ゲーム内のワールドデータのバックアップを毎日取ってきているため、これを時刻指定で参照して拾ってこれさえすれば、過去を再現した世界を見ることができるようになります!!

さっそく作れるのか技術課題を明らかにしてみましょう。

## 過去データ形式

日付ごとにファイルが作られています。
ワールドごとにとびとびなので、日付を決めただけでは、正しい過去の日付を見つけることができない

リストを日々更新することになるが、ファイル一覧をなめるのは無駄が多い
そこで、バックアップ処理で追記していく json を Title Internal に置くことにしてみましょう

また、TitleInternalData の規則に変な奴がいるので、CubeDataLife としてリネームしておくことにします。
関連処理の lambda 実装を追って、移行計画も立ててみましょう

キー名を統一するだけに見えたので、いきなり試行 朝3時に定期実行が来るので、様子を見ます→キー使用コードの差し替え漏れでバックアップが機能しませんでした。
修正したので次の日こそ!

構想では 2020年7月21日が紀元とされていて、そこから暦を一日ずつ刻むことにして
[1, 2, 3, 4, 5, 9, 100, 101, 200, 209]
といった形で配列を刻みます

TimeSpan span = DateTime.UtcNow- new DateTime(2020/7/21) ;
span.Days //差の日数

時刻から日数、日数よりさかのぼって発見する記録日数が見つけられたなら
日数を紀元に足して記録日を求める
DateTime today = new DateTime(2020/7/21).AddDays(span.Days);

var suffix = date.ToString("_yyyy-MM-dd");
で求めたい過去のデータを見つけられる気がする

まずは一度 s3 のファイルリストを取得するところからですね

ファイルリストは取れました。求めていたファイル名にも復元できます。

続いてクライアントからどうやって取り出すのか

## タイムドアアイテム

インスタントアイテムとして、ガフの扉を配置すると、日時を指定できるようになり (2020/7/21 ~ 今日まで)
選ぶと同時に扉が上昇しながら大きくなり 天井で巨大化
周囲のキューブが崩れ、自身と共に扉に向かって浮き上がっていき ゆっくりとホワイトアウトすると

過去世界データを引いて、世界が再構築され、その世界に扉から今度は落下して着地する
以後はもう一度ガフの扉を開くまで、過去世界でアイテムを増やすことも減らすこともできない世界に取り残されて、残らない過去への干渉を続けるだけになる

ガフの扉で最大の今日の日付を選ぶと、元の世界に戻ってくる

というの作るとしたら

## サーバー側の仕事

戻りたい過去の日付をサーバーに送り、過去の世界のデータダウンロード url を発行して返す という仕組みが世界データロードで差しかわれば
キャッシュを残さずに動けるかも

ということで、s3 の一次的なダウンロード url の発行というものを行います

もろもろできましたね
リリースしました

CubeArtWorld:ユーザーショップ機能

長らく作ってる姿をお見せしてきたゲームは、ついに昨年クリスマスにリリースしました!
booth.pm

ひそかにブログを見守ってくれたそこのあなたに、ぜひ買って遊んで、口コミで世に広めてもらいたいです。ご協力お願いします。

今回は、そのゲームタイトル「 Cube Art World 」に入る大型アップデート:ユーザーショップ機能について構想から記していきます。

ユーザーショップデータ

プレイヤーがキャンバスキューブと呼ばれる、所有権を有するキューブにアイテムと価格設定を加えて在庫とともに陳列するというデータをサーバー側にて刻むとすると
さて、このデータをどこに置くか どうやって他プレイヤーがアクセスするかについて考えていきたいと思います

現在のキャンバスキューブのメタデータにショップアイテム配列を追加する というのもありかもしれません。
別に用意して、データを取りに行ってあるかどうか確かめる方法もあるのですが、これは UI について決めていくと、そのハマり具合がわかり、決まりそうです。

ということで、現在のキャンバス UI から見ていきましょう。

f:id:simplestar_tech:20210214135022p:plain
キャンバス閲覧時

こちら、編集することができる者だけ Edit ボタンを押して次の編集UI に移行することができます。

f:id:simplestar_tech:20210214135206p:plain
キャンバス所有者のみが開ける編集画面

既存の UI はそのままに、ここにユーザーアイテムショップ機能をつけるとして、発想としてはショップを開くボタンを置いてみるとします。

メインの Cube Type を Canvas として、これをドロップダウンリストで Shop に切り替えられるとしましょう
この考え方の良いところは、既存の UI 設計を崩さず、新しい UI に切り替えて提供できる点です。

今の状態をそのままに、リストディフォルトを Canvas とします。データに無い場合はこれ
そして、Shop に切り替えたときに、既存の UI はすべて消え、新しいショップ用 UI に切り替わります。

そこにはアイテム選択と価格、在庫数を入れるフォームを用意し、Add ボタンでリストに追加
既存リスト項目の編集、削除が行えることにします。前後移動も可能な左右やじるしボタンも用意

項目はサムネイルとアイテム名 キャンバスの場合はタイトル 簡易説明が付く 価格 GD と、販売予定個数

あと、今後購入履歴を過去 5件ほど記録して、これをプレイヤーが認識できることとします

保存する情報はどこか
メタデータには ドロップダウンの選択項目のみ記録して、配置パスは分けます
キャンバスIDで管理はされますので、キャンバスから参照、編集には困らない

タイプがキャンバスの場合は既存のフロー
ショップの場合は先行してショップ情報を引き出して、これを使って、ショップ用の UI を表示することにします。

ドロップダウンはこの通り

f:id:simplestar_tech:20210214151853p:plain
CubeType選択

UI の行き来を作ってみましょう

販売項目追加において、いったいプレイヤーは何のアイテムを売ることができるのか?
これについて、インベントリ内の運営規定IDのキャンバスと、自身が所有権を持つキャンバスに値を付けて販売できるものとします。

インベントリアイテムリストを絞り込めるかチェックしてみます

リストを選ぶ UI を先に作っていきました

f:id:simplestar_tech:20210214181904p:plain
陳列項目追加 UI

UI の切り替えがうまくいったところで、まずはキャンバスのキューブタイプ情報を既存のメタデータに追加します。
これを保存できるようにして、さらに読み込んでから UI をそのタイプで初期化して表示するように作り替えてみましょう。

続いて、自身の所持金額を左上に表示します
もろもろ初期表示まわりがそろいましたので、続いて、 Add ボタンを押したときに 未設定の陳列アイテム 選びなさい UI が現れるようにします。
ここでSelect を押したとき、インベントリのようで、そうではない所持アイテム一覧のうちフィルターされた ショップ陳列可能なアイテムが並ぶようにしてみます

陳列項目を追加、編集するという UI だけできてきました。

f:id:simplestar_tech:20210216000941p:plain
追加操作を提供 不正があれば理由を示します。

こちらの機能は
simplestar-game.booth.pm

リリースされました!!
経済という概念、プレイヤー同士が金銭でアイテムをやり取りし出すことがはじまります

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 にはワールド名をキーにチケットの日数を与える

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

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