simplestarの技術ブログ

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

PlayFab:CloudScriptの実行頻度からクライアントを改竄したリクエストを無視する

前置き

自作ゲームの PlayFab におけるチート行為防止の話です。

f:id:simplestar_tech:20191227192113p:plain
現在の見た目(ここのところ変わってませんね)

キューブで構成される世界データをたった一つのサーバーに格納しています。
simplestar-tech.hatenablog.com

今のところユーザーからのリクエストはほぼすべて信用して処理しているのでかなり危険なつくりとなっています。
最近ゲームの仕様として、最も優れたゲーム内ツールを使ったとしても、一秒はブロックの破壊に要することに決めました。

となると、あるユーザーがキューブに対するリクエストを連続で行う場合に、一秒より短いリクエストをしてきた場合
これはクライアント改竄にほかなりません(またはコンピュータ内の時間の流れを速めているチート行為かもしれません)

別に最強すぎるピッケルとか作ってゲーム内で効率的に地面を掘ってもらってもよいのですが、データを集約しているサーバーにアクセスが集中してしまうのは
ほかのお客さんに対して不利益が生じてしまいます。アクセスが遅くなったり、貴重なアイテムの争奪戦で常に負かされてしまったりと

そこで、PlayFab の CloudScript でそういう高頻度アクセスの対処方法って何がベストなのかな~と調べたのが始まりでした。

UserInternalData に前回のリクエスト時刻を打つ

見つけたやり取りがこちら

どうやら UserInternalData にユーザーごとの情報を格納して、なんとか検知してみるといいとのことです。
自分は UserInternalData に LastRequestTime を記入して、次のリクエスト時にこれを参照して、ちゃんと 1秒以上経っていることを確認してからリクエストを処理し始めることにしました。
実験して気づけましたが、for文などでほぼ同時にリクエストすると、InternalData に書き込み終える前にリクエストを並列で処理し始めてしまいます。
そんなときのために DataVersion が確実にインクリメントされていることをチェックしています。(これ必須でした)

実際に動作確認できたコードがこちら

// cubedata request
handlers.requestCubeData = function(args, context) {
  const now = Date.now();
  // get last request time
  const lastTimeKey = "LastRequestTimeCubeData";
  const getInternalDataResponse = server.GetUserInternalData({
    PlayFabId: currentPlayerId,
    Keys: [lastTimeKey]
  });
  // request must have 1000 ms interval
  const lastTimeData = getInternalDataResponse.Data[lastTimeKey];
  if (null != lastTimeData) {
    const lastTime = Number(lastTimeData.Value);
    if (1000 > now - lastTime) {
      return JSON.stringify({
        status: 400,
        message: "called in interval"
      });
    }
  }
  // imidiately set last request time
  const updateInternalDataResponse = server.UpdateUserInternalData({
    PlayFabId: currentPlayerId,
    Data: {
      [lastTimeKey]: now
    }
  });
  // check DataVersion up 1
  if (
    updateInternalDataResponse.DataVersion !=
    getInternalDataResponse.DataVersion + 1
  ) {
    return JSON.stringify({
      status: 401,
      message: "Data inconsistency"
    });
  }

  // do something

クライアントを改竄してチート検出できているかチェック

f:id:simplestar_tech:20191227191344p:plain
連続で8回リクエストしたうち、一回だけ成功したので、大成功

まとめ

知見としては、連続して CloudScript を実行すると、関数は並列で走ることが裏付けされたことが大きい収穫

また DataVersion が関数内で正しくインクリメントされたケースを使えば、特定ユーザーの高速連続リクエストの不正なものだけを拒否できました。
良い例を示せたんじゃないかな?

Unity:ゲーム開始前にロジックでヒエラルキー編集

概要

アイテムのスロットを結構な数 たとえば 81 個とか追加して、連番で名前を打つといった作業
3~4個リネームしている途中で手を止めて、プログラムに任せたくなりますよね(守るべきは時間ではなく、心)
PlayMode にせずにスクリプトからヒエラルキーをロジックで編集する方法を記録してみたいと思います。

ExecuteInEditMode 属性をつける

このクラスは非ゲーム実行中も動くよと宣言

using UnityEngine;

[ExecuteInEditMode]
public class SlotAdder : MonoBehaviour
{
    public GameObject prefabSlot;
    public Transform parent;
    public int index;

    public void AddSlot()
    {
        var button = Instantiate(prefabSlot, parent);
        button.name = "slot" + index.ToString("00");
        index += 1;
    }
}

CustomEditor 属性をつける

さっきの AddSlot をボタン押したときに実行できると楽ですよね。
インスペクターにボタンを置いて、そこからクラスの関数を呼び出すことができます。

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(SlotAdder))]
public class SlotAdderButton : Editor 
{
    public override void OnInspectorGUI()
    {
        // 元のインスペクター部分を表示
        base.OnInspectorGUI();

        // targetを変換して対象を取得
        var slotAdder = target as SlotAdder;

        // publicなメソッドを実行するボタン
        if (GUILayout.Button("Add Slot"))
        {
            slotAdder.AddSlot();
        }
    }
}

ボタンの見た目

f:id:simplestar_tech:20191222222701p:plain
今から頑張るようなことじゃないかも

Unity:Animatorからステートを取得

前書き

ゲーム開発していて、入力モードが増えてきて、ステート管理していきたいと思う中
Unity の State Machine って Mecanim の AnimatorController だなぁと気づき
デバッグ時にステートを遷移を可視化しつつ、これをスクリプトから操作や取得できたらいいなというのが
この記事のモチベーションです。

Animator からステート名は取れない

どうもステート名を外から取るには Asset をロードしなければならないとのことであきらめ
Enum 値とステート名のハッシュ値の関連から、なるべくコストをかけずに Enum 値として State を見極められるようにしたいと思います。

アイディアの具現化

次のような Explore(探索モード)を必ず通って各ステートへ切り替わるステートマシンを用意します。
トリガーはステート名を指定すると、そのステートへ移動する設定

f:id:simplestar_tech:20191215163853p:plain
AnimatorControllerの例

これをスクリプト側で、現在のステートと次のステートへの移動を Enum で取り扱えるように工夫したものがこちら

using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// ゲーム内の入力モード・ステート管理
/// </summary>
public class InputModeStateMachine : MonoBehaviour
{
    void Start()
    {
        // ステートマシンを Layer 0 に持つ AnimatorController を持つ Animator を期待
        this.animator = GetComponent<Animator>();
        // 各ステート名は Enum 名と同じように作られていることを期待
        for (InputModeState stateEnum = InputModeState.Explore; stateEnum < InputModeState.Max; stateEnum++)
        {
            var fullPath = INPUT_STATE_LAYER_NAME + "." + stateEnum.ToString();
            var hash = Animator.StringToHash(fullPath);
            this.stateHashToEnum.Add(hash, stateEnum);
        }
    }

    // Update is called once per frame
    void Update()
    {
        // スペースキーを押すと、ステートが切り替わる
        if (Input.GetKeyDown(KeyCode.Space))
        {
            // 現在のステート情報
            var stateInfo = this.animator.GetCurrentAnimatorStateInfo(INPUT_STATE_LAYER_INDEX);
            // ハッシュ値から Enum 値へマップ
            var currentState = this.stateHashToEnum[stateInfo.fullPathHash];
            Debug.Log(currentState.ToString());

            // Explore を中継にステートを切り替える(デバッグ機能)
            if (currentState != InputModeState.Explore)
            {
                this.animator.SetTrigger(InputModeState.Explore.ToString());
                Debug.Log(InputModeState.Explore.ToString());
            }
            else
            {
                this.animator.SetTrigger(nextState.ToString());
                Debug.Log(nextState.ToString());
                this.nextState = this.nextState + 1;
                if (InputModeState.Max == this.nextState)
                {
                    this.nextState = InputModeState.Explore + 1;
                }
            }
        }
    }

    /// <summary>
    /// ステート名
    /// </summary>
    public enum InputModeState
    {
        Explore = 0,
        Inventory,
        Mining,
        TextChat,

        Max
    }

    // デバッグ用
    InputModeState nextState = InputModeState.Inventory;
    /// <summary>
    /// ステート名HashからステートEnumへのマップ
    /// </summary>
    Dictionary<int, InputModeState> stateHashToEnum = new Dictionary<int, InputModeState>();
    /// <summary>
    /// レイヤー名(fullpath でプリフィックスに利用)
    /// </summary>
    const string INPUT_STATE_LAYER_NAME = "InputStateLayer";
    /// <summary>
    /// ステートマシンのレイヤーインデックス
    /// </summary>
    const int INPUT_STATE_LAYER_INDEX = 0;
    /// <summary>
    /// ステートマシンのコントローラを持つアニメータ
    /// </summary>
    Animator animator;
}

まとめ

期待通り、スペースキーを連打すると
Explore → Inventory → Explore → Mining → Explore → TextChat → Explore
というステート遷移のデバッグログが流れていきます。

別にスクリプトだけでステート遷移してもよかったんですけど
可視化することと、その可視化したものでステート管理することで、変なステート遷移をコードに仕込まなくて済むなぁと
ちょっと回りくどかったかな

CubeWalk:カスタムデータを更新するリクエスト CloudScript の実装

構想7ステップの4つ目

半月前の構想をまとめたものがこちら
simplestar-tech.hatenablog.com

今回は表題の通り Microsoft PlayFab の CloudScript を用意して、クライアントで行うアイテムslot変更操作をサーバーに反映させます。
クライアントでスロット操作を計算して、リクエストした後、サーバー側でそのリクエストが正しいか確認してデータを変更します。

f:id:simplestar_tech:20191215011407p:plain
アイテム操作の図

クライアントからのリクエス

サーバーではアイテムスロットの総数が、現在のアイテムの個数と一致していることを判定したいので
リクエストには現在のスロットすべての合計値を渡さなければなりません

アイテムモードへの移行と、アイテムモード解除イベントを取得し
アイテムモード解除時に、変更を加えたアイテムについてのカスタムデータの更新をリクエストすることにします。

アイテムモードのオンオフ

E キーだったかな
>プレイヤーのインベントリはEキーを押すことで開くことが出来る。インベントリはEscキーでも閉じることが出来る

誰が E キーイベントをキャッチするのか
UserInventory クラスかな

次の InputSystem の実装で、UIの表示・非表示は問題なく動きます

            this.inventoryAction.performed +=
            ctx =>
            {
                this.panelInventory.gameObject.SetActive(true);
            };
            this.escapeAction.performed +=
            ctx =>
            {
                this.panelInventory.gameObject.SetActive(false);
            };

Eキーでインベントリが表示されるときと Escape で Inventory が閉じられるときにイベントを発行するようにしておきます

インベントリを表示するときに最新データを取得

前々回
simplestar-tech.hatenablog.com
の実装を関数化してこんな感じでイベントハンドラとして登録するだけ

            this.onOpenInventory += this.OnOpenInventory;

            this.inventoryAction.performed +=
            ctx =>
            {
                if (!this.panelInventory.gameObject.activeSelf)
                {
                    this.panelInventory.gameObject.SetActive(true);
                    this.onOpenInventory?.Invoke();
                }
                
            };

インベントリを閉じたときに、CustomData 変更をリクエス

まず、サーバー側の CloudScript を次のように実装していました。
詳しくは前々回
simplestar-tech.hatenablog.com


こんな感じで、クライアントからはサーバーにあるアイテム数と一致するように各スロットに振り分けたアイテム数を実際のアイテム数となるように調整しなければならない(必ず正の整数で)

// Update Item Slot
handlers.updateItemSlot = function (args, context) {
    // integrate slot item count
    var pattern = /slot([0-9]{2})$/;
    var slotTotal = 0;
    for (key in args.data) {
        var match = key.match(pattern);
        if (null != match) {
            var value = args.data[key];
            if (false == isNaN(value)) {
                if (0 < value) {
                    slotTotal += args.data[key];
                }
            }
        }
    }
    // get item remainingUses
    var remainingUses = null;
    var playerInventory = server.GetUserInventory({ PlayFabId: currentPlayerId });
    var length = playerInventory.Inventory.length;
    for (var i = 0; i < length; i++) {
        var item = playerInventory.Inventory[i];
        if (item.ItemInstanceId == args.itemInstanceId) {
            remainingUses = item.RemainingUses;
            break;
        }
    }

    // update CustomData
    var response = { status: 400 };
    if (null != remainingUses && remainingUses == slotTotal) {
        var request = {
            PlayFabId: currentPlayerId,
            ItemInstanceId: args.itemInstanceId,
            Data: args.data,
            KeysToRemove: args.keysToRemove
        };
        server.UpdateUserInventoryItemCustomData(request);
        response = { status: 200 }
    }
    var jsonString = JSON.stringify(response);
    return jsonString;
};

であるからして、クライアントは CustomData つまりはスロットのアイテム数に変更を加えたアイテムを覚えておく必要があり
そのアイテムについてスロットの設定をすべて列挙してリクエストしなければならない

気づいたことに、前々回の対応には不備としてアイテムが増えたときの CustomData の修正が抜けていたが
後で考えることにします。

今はリクエストをなげて、その不備を確認するところから

クライアントでこれを実現するとなると、このようなコードになります。

        /// <summary>
        /// スロットをクリックしたときのアイテムのホールド操作
        /// </summary>
        /// <param name="slotIndex">クリックしたスロット</param>
        private void OnClickSlot(int slotIndex)
        {
            // スロットとホルダーを交換
            var item = this.items[slotIndex];
            this.items[slotIndex] = this.items[MAX_SLOT_INDEX];
            if (null != this.items[slotIndex])
            {
                this.items[slotIndex].slotIndex = slotIndex;
            }
            this.items[MAX_SLOT_INDEX] = item;
            if (null != this.items[MAX_SLOT_INDEX])
            {
                this.items[MAX_SLOT_INDEX].slotIndex = MAX_SLOT_INDEX;
            }

            // スロットの CubeObject を現在の状態となるように更新
            Destroy(this.slotCubes[slotIndex]);
            var cubeObject = this.CreateSlotCubeObject(this.items[slotIndex]);
            if (null != cubeObject)
            {
                cubeObject.transform.SetParent(this.slotTransforms[slotIndex]);
                cubeObject.transform.localPosition = Vector3.zero;
                cubeObject.transform.localScale = new Vector3(50, 50, 1);
            }
            this.slotCubes[slotIndex] = cubeObject;

            // ホルダーの CubeObject を現在の状態となるように更新
            Destroy(this.slotCubes[MAX_SLOT_INDEX]);
            this.slotCubes[MAX_SLOT_INDEX] = this.CreateSlotCubeObject(this.items[MAX_SLOT_INDEX]);
            if (null != this.slotCubes[MAX_SLOT_INDEX])
            {
                this.slotCubes[MAX_SLOT_INDEX].transform.SetParent(this.panelInventory);
                this.slotCubes[MAX_SLOT_INDEX].transform.localScale = new Vector3(25, 25, 1);
            }
            // 編集したアイテムとして記録
            if (null != item && null != item.item && !this.editItems.ContainsKey(item.item.ItemInstanceId))
            {
                this.editItems.Add(item.item.ItemInstanceId, item);
            }
        }

上記のようにアイテムを記録しておいて、インベントリを閉じるときに次の通り、アイテムスロットの更新をリクエストします。

        private void OnCloseInventory()
        {
            // 編集したアイテムのインスタンスIDを持つスロットをすべて収集
            foreach (var edit in this.editItems)
            {
                var itemInstanceId = edit.Key;
                List<ViewItem> viewItemList = new List<ViewItem>();
                foreach (var view in this.items)
                {
                    if (null != view && null != view.item)
                    {
                        if (0 == string.Compare(view.item.ItemInstanceId, itemInstanceId))
                        {
                            viewItemList.Add(view);
                        }
                    }
                }
                // 収集したスロット情報で上書きをリクエスト(スロット表記と実際のアイテム数が合えば承認されます)
                this.UpdateUserInventoryItemCustomData(edit.Value.item, viewItemList);
            }
            this.editItems.Clear();
        }

UpdateUserInventoryCustomData 関数はこんな感じ(前々回に作った)

        /// <summary>
        /// アイテムのカスタムデータ更新リクエスト
        /// </summary>
        /// <param name="item">対象のアイテム</param>
        /// <param name="viewItemList">カスタムデータがなかったのでやむを得ず空いてるスロットにセットしたアイテム一覧</param>
        internal void UpdateUserInventoryItemCustomData(ItemInstance item, List<ViewItem> viewItemList)
        {
            List<string> keysToRemove = new List<string>();
            if (null != item.CustomData)
            {
                foreach (var keyValuePair in item.CustomData)
                {
                    var match = Regex.Match(keyValuePair.Key, "slot([0-9]{2})");
                    if (match.Success)
                    {
                        bool removeTarget = true;
                        foreach (var viewItem in viewItemList)
                        {
                            var newKey = $"slot{viewItem.slotIndex.ToString("00")}";
                            if (keyValuePair.Key == newKey)
                            {
                                removeTarget = false;
                                break;
                            }
                        }
                        if (removeTarget)
                        {
                            // スロットのキーパターンで、今回の上書きキーとは別のキーは削除する(スロットの移動対応)
                            keysToRemove.Add(keyValuePair.Key);
                        }
                    }
                }
            }
            Dictionary<string, string> customData = new Dictionary<string, string>();
            foreach (var viewItem in viewItemList)
            {
                var newKey = $"slot{viewItem.slotIndex.ToString("00")}";
                customData[newKey] = viewItem.count.ToString();
            }
            var itemInstanceId = item.ItemInstanceId;
            PlayFabCloudScript.UpdateItemSlotRequest(itemInstanceId, customData, keysToRemove, (result) => {
                Debug.Log($"result.status = {result.status}");
            });
        }

うまくいかないケースとは

たとえばブロックを破壊したときにサーバー側でインベントリのアイテム数が増えますが
CustomData のスロットの所属数は増えません(そこまで面倒見切れない)

となると、アイテム操作に不正がなくても、スロットの所属情報である CustomData を持っている時点で
ブロックを破壊したものなら、次回のアイテム操作がすべて不正操作扱いとなってしまいます。
あと、せっかくアイテムをゲットしたのに、インベントリを開いても数が増えてない

これを解決するには

・アイテムをゲットしたときに CustomData をインクリメント → アイテムゲットはブロック破壊や配置で発生するので、そのような計算コスト払いたくない
・インベントリを開いたときに、合計スロット所属数とアイテム数に差異があったら、訂正してあげる

後者ならクライアントサイドの計算なので大丈夫ですね。
こちらの対応を進めてみます。

こんな感じで、コメントにあるところに、期待する処理を入れることにします。

                    bool hasSlot = false;
                    if (null != item.CustomData)
                    {
                        // スロット指定のあるアイテム数を集計
                        int totalSlotCount = 0;
                        foreach (var customData in item.CustomData)
                        {
                            var match = Regex.Match(customData.Key, "slot([0-9]{2})");
                            if (match.Success && 2 == match.Groups.Count && int.TryParse(customData.Value, out int itemCount))
                            {
                                int slotIndex = int.Parse(match.Groups[1].Value);
                                this.items[slotIndex] = new ViewItem { slotIndex = slotIndex, item = item, count = itemCount };
                                totalSlotCount += itemCount;
                                hasSlot = true;
                            }
                        }
                        // もしスロット数と不一致の場合は正す
                        if (totalSlotCount != item.RemainingUses)
                        {
                            Debug.Log("不一致");
                            // 減らすときは、追加したスロットの先頭から数を減らしていくといいかな、ゼロになったら、次に小さいスロット番号のスロットから数を減らしていく
                            // 増やすときも、先頭から数を増やしていき、MAXになったら次のスロットに埋めていく、もしすべてのスロットがMAXになったら、余っているスロットにアイテムを入れる
                            // 全部処理を終えたら、サーバーにリクエストしておく
                        }
                        else
                        {
                            Debug.Log("一致してます");
                        }
                    }
                    // スロット情報を持たないアイテムを記録
                    if (!hasSlot)
                    {
                        noSlotItems.Add(item);
                    }

ただ、このやり方だと増えたときにもしすべてのスロットが埋まっていたとき、もしオーバーした分を切り捨てたら
サーバーからの承認を一生得られないことになるんですよね(一応、該当アイテムを消費していけば、次のインベントリオープンで回避できるけど)

どんなときにも承認を得られるようにするために、オーバー分をそのまま個数無限でオーバー分として保存できるようにしようと思います
ユーザーからはそのオーバー分は取り戻せるようにしてあげればなんとか

あ、もう一ついい方法を思いついた。
ローカルでキューブを破壊したときに、ローカルでスロット情報をインクリメントして、これをサーバーにリクエストする
これならサーバー負荷が上がらず、クライアントが正しい計算結果をリクエストし続ける限り困る人はいない

一応、何かの拍子で壊れてしまった時だけ操作不能にならない対応を入れることにした。
コメント部分を具体化すると次の通り

// もしスロット数と不一致の場合は正す
if (hasSlot && totalSlotCount != item.RemainingUses)
{
    // 減らすとき、増やすときも、いったんローカルのアイテム情報をすべてクリアして CustomData のスロット情報が無かったことにする
    List<ViewItem> viewItemList = new List<ViewItem>();
    for (int slotIndex = 0; slotIndex < this.items.Length; slotIndex++)
    {
        if (null != this.items[slotIndex] && null != this.items[slotIndex].item && 0 == string.Compare(this.items[slotIndex].item.ItemInstanceId, item.ItemInstanceId))
        {
            this.items[slotIndex] = null;
        }
    }
    hasSlot = false;
}

まとめ

Microsoft PlayFab の CloudScript を用意して、クライアントで行うアイテムslot変更操作をサーバーに反映させ
クライアントでスロット操作を計算して、リクエストした後、サーバー側でそのリクエストが正しいか確認してデータを変更しました。

Eキーでインベントリが開き、Esc キーで閉じます
開いたときにサーバーから最新のインベントリ情報を取得して表示し
Esc キーでインベントリを閉じたときに変更したアイテムに関してのみ操作内容の反映をリクエストします。

正しく動きました。
構想も完璧じゃなくて、さっきひらめいた実装アイディアだと
アイテムを置いたりブロックを破壊したりしたときのアイテムスロットの増減調整をクライアントコードに書かないといけないことになります

次は 4.5 ステップとして、ブロック配置とブロック破壊におけるアイテムスロットの残数の増減を書くことにします。

CubeWalk:マウス操作でインベントリのアイテムを移動できる機能

概要

半月前に構想した内容がこちら
simplestar-tech.hatenablog.com

前々回、前回で進んだ内容が以下
1.R Ctrl でキューブ、SideA, SideB キューブと配置形状が切り替わる
2.オンラインのインベントリ情報を受け取って表示する機能

今回は 3/7 ステップとして
3.マウス操作でインベントリのアイテムを移動できる機能

を作ります。

構想した手順

スロットをクリックして
アイテムがあればアイテムをホールド、なければ何も起きない

クリックを取得

まずは、スロット側にクリックイベントで何か関数呼び出しできるようにしてみます

using UnityEngine;
using UnityEngine.UI;

public class ItemSlot : MonoBehaviour
{
    void Start()
    {
        this.button = GetComponent<Button>();
        this.button.onClick.AddListener(this.OnClick);
    }

    public void OnClick()
    {
        Debug.Log("clicked " + this.gameObject.name);
    }

    Button button;
}

Image UI に Button コンポーネントをつける必要あり

ところでキューブが配置されても、呼ばれる?→はい、大丈夫

ホールドとは

クリックしたスロットにアイテムがあるか見て
ある場合は、そのアイテムをホールド
特別な入れ物にスロットのアイテムを全部入れて、元のスロットを空にする

実装のイメージがわいたのですが
slot の uGUI の slot に Button コンポーネントを静的にアタッチしておき、名前で解決している部分で、OnClickイベントハンドラをセットします。
そうすれば、すべてのスロットのクリックを取得することができ、スロットが空なのかどうか判定できる要素にアクセスできます。

以下が、具現化したコード 期待通り動きました。

        // Start is called before the first frame update
        void Start()
        {
            // パネルに並ぶslot名からimage配列を作成
            foreach (Transform slotImage in this.panelInventory)
            {
                var match = Regex.Match(slotImage.name, "slot([0-9]{2})");
                if(match.Success && 2 == match.Groups.Count)
                {
                    if(int.TryParse(match.Groups[1].Value, out int slotIndex))
                    {
                        this.slotTransforms[slotIndex] = slotImage;

                        // クリックイベントハンドラを登録
                        var button = slotImage.GetComponent<Button>();
                        if (null != button)
                        {
                            button.onClick.AddListener(() => { this.OnClickSlot(slotIndex); });
                        }
                    }
                }
            }
        }

        private void OnClickSlot(int slotIndex)
        {
            Debug.Log("clicked " + slotIndex);
        }

スロットが空とは?
null でしょうか? → はい

このように見分けます。

        private void OnClickSlot(int slotIndex)
        {
            Debug.Log("clicked " + slotIndex);
            var viewItem = this.viewItems[slotIndex];
            if (null != viewItem && 0 < viewItem.count)
            {
                Debug.Log("itemName " + viewItem.item.DisplayName);
            }
        }

ホルダー

ホールドするときは特別なホルダーと呼ばれる ItemView 保管庫に ItemView が格納されます
それだけ

アイテムがあるスロットをクリックしたときに発生し
ホルダーにアイテムがあるときは、交換するようにホールドします

ホルダーは具体的には ItemView クラス変数
nullable です

ビューを更新するには?

render 関数を作って、データに更新があったら render するというのを考えたのですが、それだとキューブメッシュ作成が 81 回も行われてしまうので
変化が生まれるクリックしたスロットにのみ、キューブオブジェクトを除去したり、生成して設置したりすることにします。

f:id:simplestar_tech:20191212221921p:plain
クリックによってホールド、配置が行えるようになりました

デバッグしていて思ったことは、サーバーから情報を取得したときにアイテムslotの状況が残ってしまい、図のようにローカルでアイテムが複製されてしまいました。
サーバーから情報を引いてくるときにクリアします。

ホールド中はアイテムがカーソルに付随

ホールドしたアイテムが null じゃないなら

f:id:simplestar_tech:20191213002125p:plain
できた

こんな感じのコードでマウスカーソルについてくる形です。

        private void OnClickSlot(int slotIndex)
        {
            Debug.Log("clicked " + slotIndex);
            var item = this.items[slotIndex];
            this.items[slotIndex] = this.itemHolder;
            this.itemHolder = item;

            // スロットの CubeObject を再作成
            Destroy(this.slotCubes[slotIndex]);
            var cubeObject = this.CreateSlotCubeObject(this.items[slotIndex]);
            if (null != cubeObject)
            {
                cubeObject.transform.SetParent(this.slotTransforms[slotIndex]);
                cubeObject.transform.localPosition = Vector3.zero;
                cubeObject.transform.localScale = new Vector3(50, 50, 1);
            }
            this.slotCubes[slotIndex] = cubeObject;

            Destroy(this.holderCube);
            this.holderCube = CreateSlotCubeObject(this.itemHolder);
            if (null != this.holderCube)
            {
                this.holderCube.transform.SetParent(this.panelInventory);
                this.holderCube.transform.localScale = new Vector3(25, 25, 1);
            }
        }

        void OnGUI()
        {
            Event currentEvent = Event.current;
            Vector2 mousePos = new Vector2();

            // Get the mouse position from Event.
            // Note that the y position from Event is inverted.
            mousePos.x = currentEvent.mousePosition.x;
            mousePos.y = cam.pixelHeight - currentEvent.mousePosition.y;

            var point = cam.ScreenToWorldPoint(new Vector3(mousePos.x + 14, mousePos.y + 14, cam.nearClipPlane + 0.008f));
            if (null != this.holderCube)
            {
                this.holderCube.transform.position = point;
            }
        }

まとめ

アイテムビューを開いたとき、マウスカーソルが出てくるモードがあるので、そのときにクリックして
アイテムがあればアイテムをホールド、なければ何も起きない
ホールドしていた状態でクリックしたとき、スロットだったらスロットに配置する
スロットでなければ…の一歩手前まで動作確認できました。

3.マウス操作でインベントリのアイテムを移動できる機能
が動きました。

これで 3/7 の進捗ですね。
残りも頑張ります

CubeWalk:インベントリ情報を受け取って表示する機能

概要

構想によって作られたステップ 2/7 を今回はクリアしたいと思います。
simplestar-tech.hatenablog.com

表題の通り PlayFab からインベントリの情報を取得して、これを UI で表示します。

簡易なシーン

PlayFab のスクリプトをシーンに追加して試験環境を作ります。

参考ページはこちら
qiita.com

私は例によって、必ずログイン後に実行されるような仕組みを入れてみました。

    private static void GetUserInventory()
    {
        PlayFabLogin.AfterLoginCall(() =>
        {
            PlayFabClientAPI.GetUserInventory(new GetUserInventoryRequest(), result =>
            {
                foreach (ItemInstance item in result.Inventory)
                {
                    Debug.Log(item.ItemId);
                    Debug.Log(item.DisplayName);
                    if (null != item.CustomData)
                    {
                        foreach (var customData in item.CustomData)
                        {
                            Debug.Log($"key = {customData.Key}, value = {customData.Value}");
                        }
                    }
                }
            }, error => Debug.Log(error.GenerateErrorReport()));
        });
    }

ブロックのアイテムID、表示名、残数、アイテムインスタンスIDなどが ItemInstance から取得できます。
CustomData は null でした。設定前なので
スロットに 99 個までアイテムを詰め込んで、CustomData に情報がなかったら、クライアントから詰めてあげるのはどうかと考えています。
先に進めます。

キューブのテクスチャをスロットに配置

まずは見た目の話で、スロットの子オブジェクトとして Image を置き、そこに Shader を割り当てることができるか?
できると、アイテムの表示とキューブの表示が統一できて良さそうと考えました。

できます。

3Dのマテリアルを2Dに当てるのは間違いだったと思い始めました。
uGUIに3Dのモデルって置けますか?
Canvas のモードを Screen Space Camera にすることで、できました。

となると、既存のキューブ選択や、配置前のオブジェクトの作成コードって使いまわせますか?
共通部分を抜き出して Mesh 処理に任せられないでしょうか?

できます。具体的には次の通り、キューブ作成コードを関数化できました!

        /// <summary>
        /// キューブ情報からキューブオブジェクトを作成
        /// </summary>
        /// <param name="prefab">元となるprefab</param>
        /// <param name="parent">親オブジェクト</param>
        /// <param name="pData">キューブデータ4byteのポインタ</param>
        /// <param name="vertexCount">キューブ情報に基づく頂点数</param>
        /// <param name="meshShader">シェーダー</param>
        /// <returns>キューブオブジェクト</returns>
        internal unsafe GameObject InstantiateCubeObject(GameObject prefab, Transform parent, byte* pData, int vertexCount, Shader meshShader = null)
        {
            // 再作成
            var cubeObject = Instantiate(prefab, parent);

            #region マテリアルの設定
            if (null == meshShader)
            {
                meshShader = this.meshShader;
            }
            if (!this.materials.ContainsKey(meshShader.GetInstanceID()))
            {
                var material = new Material(meshShader);
                material.SetTexture("_MainTex", this.textureAlbedo);
                this.materials.Add(meshShader.GetInstanceID(), material);
            }
            var renderer = cubeObject.GetComponent<MeshRenderer>();
            renderer.sharedMaterial = this.materials[meshShader.GetInstanceID()];
            #endregion

            // キューブデータの作成
            var nativeVertices = new NativeArray<ChunkMeshSystem.CustomVertexLayout>(vertexCount, Allocator.Temp);
            var meshData = new ChunkMeshData { vertexCount = vertexCount, pVertices = (float*)nativeVertices.GetUnsafePtr() };
            ChunkMeshSystem.FillCubeVertices(pData, ref meshData);

            // メッシュ作成
            var mesh = new Mesh();
            mesh.Clear();
            // メッシュレイアウトを定義
            var layout = new[]
            {
                new VertexAttributeDescriptor(VertexAttribute.Position, VertexAttributeFormat.Float32, 3),
                new VertexAttributeDescriptor(VertexAttribute.TexCoord0, VertexAttributeFormat.Float32, 2),
            };

            mesh.SetVertexBufferParams(vertexCount, layout);
            mesh.SetIndexBufferParams(vertexCount, IndexFormat.UInt32);

            // インデックスを作成
            var nativeIndices = new NativeArray<int>(vertexCount, Allocator.Temp);
            for (int index = 0; index < vertexCount; index++)
            {
                nativeIndices[index] = index;
            }

            // 頂点データとインデックスを設定
            mesh.SetVertexBufferData(nativeVertices, 0, 0, vertexCount);
            mesh.SetIndexBufferData(nativeIndices, 0, 0, vertexCount);

            // ネイティブ配列を開放
            nativeVertices.Dispose();
            nativeIndices.Dispose();

            // サブメッシュの定義
            mesh.subMeshCount = 1;
            mesh.SetSubMesh(0, new SubMeshDescriptor
            {
                topology = MeshTopology.Triangles,
                vertexCount = vertexCount,
                indexCount = vertexCount,
                baseVertex = 0,
                firstVertex = 0,
                indexStart = 0
            });

            // 法線と接戦とバウンディングボックスを作成
            mesh.RecalculateNormals();
            mesh.RecalculateTangents();
            mesh.RecalculateBounds();

            // メッシュの設定
            var meshFilter = cubeObject.GetComponent<MeshFilter>();
            if (null != meshFilter.sharedMesh)
            {
                meshFilter.sharedMesh.Clear();
            }
            meshFilter.sharedMesh = mesh;
            return cubeObject;
        }

これを使って、インベントリスロットにキューブアイコンを設置できるようになりました!

f:id:simplestar_tech:20191124163736p:plain
キューブアイコン

でもこのキャンバスって、オーバーレイじゃないから、邪魔な背景が来ませんか?
はい、邪魔になります

やっぱり、2D 用のシェーダーに書き換えた方がいいでしょうか?
次に移ります。

UV計算するシェーダー

使うテクスチャは同じで、UV計算を正しく行ってみることにします。
計算式はこちら

               // テクスチャ座標計算に使う変数を計算
                int row = (byte)sideType % 17;
                int col = (byte)sideType / 17;

                const float shrinkAmount = 232f / 4096f;
                float offsetU = col / 4.0f;
                float offsetV = shrinkAmount * (16 - row) % 17 + 0.0371f;

                    // 回転を考慮して UV 設定
                    var pUV = pPosition + 3;
                    pUV[0] = pUV[0] * shrinkAmount + offsetU;
                    pUV[1] = pUV[1] * shrinkAmount + offsetV;

uv 値のソースは次の通り

            #region 原点中心にプリズム二本で構成されるキューブの頂点データ定義→NativeArray確保
            const float halfSide = ChunkConst.CubeSide / 2.0f;
            var vertices = new float[]{
                // Prism A                       // Top
                -halfSide, +halfSide, +halfSide, 0, 1,
                +halfSide, +halfSide, +halfSide, 1, 1,
                +halfSide, +halfSide, -halfSide, 1, 0,
                                                 // Bottom
                +halfSide, -halfSide, -halfSide, 1, 0,
                +halfSide, -halfSide, +halfSide, 0, 0,
                -halfSide, -halfSide, +halfSide, 0, 1,
                                                 // Right up
                +halfSide, +halfSide, -halfSide, 1, 1,
                +halfSide, +halfSide, +halfSide, 2, 1,
                +halfSide, -halfSide, +halfSide, 2, 0,
                                                 // Right down
                +halfSide, -halfSide, +halfSide, 2, 0,
                +halfSide, -halfSide, -halfSide, 1, 0,
                +halfSide, +halfSide, -halfSide, 1, 1,
                                                 // Forward up
                +halfSide, +halfSide, +halfSide, 2, 1,
                -halfSide, +halfSide, +halfSide, 3, 1,
                -halfSide, -halfSide, +halfSide, 3, 0,
                                                 // Forward down
                -halfSide, -halfSide, +halfSide, 3, 0,
                +halfSide, -halfSide, +halfSide, 2, 0,
                +halfSide, +halfSide, +halfSide, 2, 1,
                                                 // Cross up
                -halfSide, +halfSide, +halfSide, 3, 1,
                +halfSide, +halfSide, -halfSide, 4.4142f, 1,
                +halfSide, -halfSide, -halfSide, 4.4142f, 0,
                                                 // Cross down
                +halfSide, -halfSide, -halfSide, 4.4142f, 0,
                -halfSide, -halfSide, +halfSide, 3, 0,
                -halfSide, +halfSide, +halfSide, 3, 1,
                // Prism B                       // BottomB
                -halfSide, -halfSide, +halfSide, 0, 1,
                -halfSide, -halfSide, -halfSide, 1, 1,
                +halfSide, -halfSide, -halfSide, 1, 0,
                                                 // TopB
                +halfSide, +halfSide, -halfSide, 1, 0,
                -halfSide, +halfSide, -halfSide, 0, 0,
                -halfSide, +halfSide, +halfSide, 0, 1,
                                                 // Left up B
                -halfSide, +halfSide, +halfSide, 1, 1,
                -halfSide, +halfSide, -halfSide, 2, 1,
                -halfSide, -halfSide, -halfSide, 2, 0,
                                                 // Left down B
                -halfSide, -halfSide, -halfSide, 2, 0,
                -halfSide, -halfSide, +halfSide, 1, 0,
                -halfSide, +halfSide, +halfSide, 1, 1,
                                                 // Back up B
                -halfSide, +halfSide, -halfSide, 2, 1,
                +halfSide, +halfSide, -halfSide, 3, 1,
                +halfSide, -halfSide, -halfSide, 3, 0,
                                                 // Back down B
                +halfSide, -halfSide, -halfSide, 3, 0,
                -halfSide, -halfSide, -halfSide, 2, 0,
                -halfSide, +halfSide, -halfSide, 2, 1,
                                                 // Cross up
                +halfSide, +halfSide, -halfSide, 3, 1,
                -halfSide, +halfSide, +halfSide, 4.4142f, 1,
                -halfSide, -halfSide, +halfSide, 4.4142f, 0,
                                                 // Cross down
                -halfSide, -halfSide, +halfSide, 4.4142f, 0,
                +halfSide, -halfSide, -halfSide, 3, 0,
                +halfSide, +halfSide, -halfSide, 3, 1,
            };

想像することはできると思います。
描画結果を確認したところ真っ黒でした。

あれ、なんで GameView だけ真っ黒なのでしょう?
それは、こういう落とし穴でした。
https://forum.unity.com/threads/shader-graph-ui-image-shader.628111/

解決には Screen Space Camera のレンダーモードにするしかないらしい
となると、先ほどのキューブメッシュ作成関数と同じあきらめる理由になります。

ここにきて、またキューブ作成案が浮上!

邪魔になる問題は残りますが
Plane Distance をカメラの near plane ぎりぎりまで近づければなんとかなるので、それでいくことに!

ちょっと無駄骨でしたが、次の ShaderGraph はお蔵入りです。

f:id:simplestar_tech:20191125081334p:plain
上記のUV計算する ShaderGraph

Cube メッシュの対応を行えば Inventory のアイコンも完成するはずで
最後に笑うことができそうです。うれしい!

InventoryCanvas

キャンバスのモードが違うものを用意

f:id:simplestar_tech:20191125081035p:plain
Screen Space Camera でうまくできたけど…

さきほどからゲーム内だけ真っ黒になる話題がありますが
UIにだけ Camera 側から Directional Light を放つ感じで用意します。
そうしないと、暗闇で作業したときに UI も暗くなっちゃうことになりますので

さて

これはキューブに UI レイヤーを適用して、directional light を ui だけに当てれば解決します。
また、既存の directional light は ui に当てないことを注意する必要があります。
これで解決することができました。

まだスロットを決定していないアイテムの場所

CustomData の値を頼りにスロットにアイテムを置こうとしていますが、最初は CustomData は null です。

すべてがふわふわしている状況ですが、まずどこに置くか決めていきます。

ホットバーを作ります。

ホットバーの空いている若い番号
99個オーバーしない限りは、そのスロット番号を CustomData に詰めるとし

99個オーバーしたら、次の若いスロット番号を使うことにします。

ホットバーは特別なスロットではなく 0~8 のインデックスが振られたただのスロットということにします。

そろそろ手が動かせるところなのでスロットの背景画像用の png ファイルを用意します。
ClipStudio で作ります。

これを Grid Layout で並べてみました
左下から右上にかけて、みぎに並ぶように slot インデックスが振られています。(そういうレイアウト指定が可能)

f:id:simplestar_tech:20191208153205p:plain
スロット表示

ゲーム開始時にスロット番号にあった配列を作ります。

        // Start is called before the first frame update
        void Start()
        {
            // パネルに並ぶslot名からimage配列を作成
            foreach (Transform slotImage in this.panelInventory)
            {
                var match = Regex.Match(slotImage.name, "slot([0-9]{2})");
                if(match.Success && 2 == match.Groups.Count)
                {
                    this.slotTransforms[int.Parse(match.Groups[1].Value)] = slotImage;
                }
            }
        }

インベントリ情報を受け取ったら
カスタムデータがあるものについて、まずはスロットに情報を埋めます。(今はありません)
slot00 とかでマッチしたら 0 スロットに value の値を入れることにします。(今はありません)

しつこいですが、最初はカスタムデータを持たないので、若いスロットからアイテムを詰めていきます。

若いスロットからアイテムを詰めていくコードはこんな感じ

                PlayFabInventory.GetUserInventory(result => {
                    this.userInventory = result.Inventory;
                    List<ItemInstance> noSlotItems = new List<ItemInstance>();
                    foreach (var item in this.userInventory)
                    {
                        if ("0" == item.ItemId)
                        {
                            continue;
                        }
                        Debug.Log(item.ItemId);
                        Debug.Log(item.DisplayName);
                        Debug.Log(item.RemainingUses);
                        if (null != item.CustomData)
                        {
                            foreach (var customData in item.CustomData)
                            {
                                var match = Regex.Match(customData.Key, "slot([0-9]{2})");
                                if (match.Success && 2 == match.Groups.Count && int.TryParse(customData.Value, out int itemCount))
                                {
                                    this.viewItems[int.Parse(match.Groups[1].Value)] = new ViewItem { item = item, count = itemCount };
                                }
                                Debug.Log($"key = {customData.Key}, value = {customData.Value}");
                            }
                        }
                        else
                        {
                            noSlotItems.Add(item);
                        }
                    }
                    foreach (var item in noSlotItems)
                    {
                        for (int viewItemIndex = 0; viewItemIndex < this.viewItems.Length; viewItemIndex++)
                        {
                            if (null == this.viewItems[viewItemIndex] && null != item.RemainingUses)
                            {
                                const int slotMaxPrismCount = 99 * 2;
                                int overSlot = Mathf.FloorToInt((int)item.RemainingUses / slotMaxPrismCount);
                                for (int i = overSlot; i >= 0; i--)
                                {
                                    if (0 != i)
                                    {
                                        this.viewItems[viewItemIndex] = new ViewItem { item = item, count = slotMaxPrismCount };
                                        for (; viewItemIndex < this.viewItems.Length; viewItemIndex++)
                                        {
                                            if (null == this.viewItems[viewItemIndex])
                                            {
                                                break;
                                            }
                                        }
                                    }
                                    else if (0 < ((int)item.RemainingUses - overSlot * slotMaxPrismCount))
                                    {
                                        this.viewItems[viewItemIndex] = new ViewItem { item = item, count = (int)item.RemainingUses - overSlot * slotMaxPrismCount };
                                    }
                                }
                                break;
                            }
                        }
                    }
                    for (int viewItemIndex = 0; viewItemIndex < this.viewItems.Length; viewItemIndex++)
                    {
                        if (null != this.viewItems[viewItemIndex])
                        {
                            this.CreateBasicCubeObject(viewItemIndex, this.viewItems[viewItemIndex]);
                        }
                    }
                });
            }

上記でいうところの viewItems に格納することによって CustomData に次から slot00:99 とかの値が入ってほしいので、ここでカスタムDataの更新リクエストをクライアントから行います。

クライアントからアイテムの CustomData を更新する

アイテム数を増やすのではなく、装備状況などをゲーム情報としてカスタム管理するので、これはクライアントから投げても ok とします。
この世界ではカスタムデータをどんなに偽装しても、最終的にはアイテム数を処理するので、整合性がとれなくなった場合は、リクエストされたカスタムデータが無視されます
PlayFab の api の情報によれば、サーバー側でしかカスタムデータを編集できないので、クライアントからは、どのslotにいくつ移動したという旨のリクエストを投げます。

アイテムに関するすべてのカスタムデータを一度に送ることを考えます。
サーバー側ではカスタムデータの総数がアイテム数より低いことを確かめてもらうためです。
サーバーは、リクエストに不整合がないことを確認し、承認する形でカスタムデータを変更します。

C# 側からのリクエストは次の通り

        /// <summary>
        /// アイテムのカスタムデータ更新リクエスト
        /// </summary>
        /// <param name="item">対象のアイテム</param>
        /// <param name="viewItemList">カスタムデータがなかったのでやむを得ず空いてるスロットにセットしたアイテム一覧</param>
        internal void UpdateUserInventoryItemCustomData(ItemInstance item, List<ViewItem> viewItemList)
        {
            List<string> keysToRemove = new List<string>();
            if (null != item.CustomData)
            {
                foreach (var keyValuePair in item.CustomData)
                {
                    var match = Regex.Match(keyValuePair.Key, "slot([0-9]{2})");
                    if (match.Success)
                    {
                        bool removeTarget = true;
                        foreach (var viewItem in viewItemList)
                        {
                            var newKey = $"slot{viewItem.slotIndex.ToString("00")}";
                            if (keyValuePair.Key == newKey)
                            {
                                removeTarget = false;
                                break;
                            }
                        }
                        if (removeTarget)
                        {
                            // スロットのキーパターンで、今回の上書きキーとは別のキーは削除する
                            keysToRemove.Add(keyValuePair.Key);
                        }
                    }
                }
            }
            Dictionary<string, string> customData = new Dictionary<string, string>();
            foreach (var viewItem in viewItemList)
            {
                var newKey = $"slot{viewItem.slotIndex.ToString("00")}";
                customData[newKey] = viewItem.count.ToString();
            }
            var itemInstanceId = item.ItemInstanceId;
            PlayFabCloudScript.UpdateItemSlotRequest(itemInstanceId, customData, keysToRemove, null);
        }

呼び出している関数はこちら

    /// <summary>
    /// アイテムのスロット情報変更をリクエスト
    /// </summary>
    public static void UpdateItemSlotRequest(string itemInstanceId, object data, object keysToRemove, UnityAction<UpdateItemSlotResult> callback)
    {
        ExecuteCloudScript<UpdateItemSlotResult>("updateItemSlot", new { itemInstanceId = itemInstanceId, data = data, keysToRemove = keysToRemove }, successResult =>
        {
            callback?.Invoke(successResult);
        });
    }

CloudScript はこんな感じ

// Update Item Slot
handlers.updateItemSlot = function (args, context) {
    var request = {
        PlayFabId: currentPlayerId, 
        ItemInstanceId: args.itemInstanceId,
        Data: args.data,
        KeysToRemove: args.keysToRemove
    };
    var response = server.UpdateUserInventoryItemCustomData(request);
    var jsonString = JSON.stringify(response);
    return jsonString;
};

サーバー側でスロットのカウントとアイテム数を精査

カスタムデータが slot xx のものの値を合計して、アイテム総数と一致しているかをチェックする機能を Cloud Script 側に入れます。
正しく status 200 が返ってきたことを確認できたコードがこちら

// Update Item Slot
handlers.updateItemSlot = function (args, context) {
    // integrate slot item count
    var pattern = /slot([0-9]{2})$/;
    var slotTotal = 0;
    for (key in args.data) {
        var match = key.match(pattern);
        if (null != match) {
            slotTotal += args.data[key];
        }
    }
    // get item remainingUses
    var remainingUses = null;
    var playerInventory = server.GetUserInventory({ PlayFabId: currentPlayerId });
    var length = playerInventory.Inventory.length;
    for (var i = 0; i < length; i++) {
        var item = playerInventory.Inventory[i];
        if (item.ItemInstanceId == args.itemInstanceId) {
            remainingUses = item.RemainingUses;
            break;
        }
    }

    // update CustomData
    var response = { status: 400 };
    if (remainingUses == slotTotal) {
        var request = {
            PlayFabId: currentPlayerId,
            ItemInstanceId: args.itemInstanceId,
            Data: args.data,
            KeysToRemove: args.keysToRemove
        };
        server.UpdateUserInventoryItemCustomData(request);
        response = { status: 200 }
    }
    var jsonString = JSON.stringify(response);
    return jsonString;
};

まとめ

ユーザーのインベントリ情報を取得、ローカルでの若い番号のスロットにアイテムを詰める方法を
サーバーにリクエストして、サーバー側で不整合がないか確認を行い、合格したときにカスタムデータを更新
次回から、そのカスタムデータを使ってスロットにアイテムを格納する

できました

f:id:simplestar_tech:20191210004706p:plain
ホットバーの若い番号にアイテムが追加されている様子

CubeWalk:R Ctrl でキューブ、SideA, SideB と配置形状が切り替わる

概要

構想に近づけるため、最初のステップを踏みます
simplestar-tech.hatenablog.com


右クリックすると無限に 2 x 2 のプリズムを配置する現在
これを右クリック中 R Ctrl キーを押すと、プリズム A, プリズム B と切り替わり
その時に右クリックを終了するとプリズムが配置されるというもの

f:id:simplestar_tech:20191124010211p:plain
プリズム単位で配置
これを実現してみます。

実装イメージ

新しい InputAction をこうやって使う感じかな

        this.modeAction.performed +=
            ctx =>
            {
                if (this.locatingFlag)
                {
                    // 配置モードを切り替える
                    this.mode++;
                    if (Mode.Max == this.mode)
                    {
                        this.mode = Mode.Cube;
                    }
                }
            };

事前配置は完成

そのまま配置すると、キューブが置かれてしまった
それもそのはず、配置時にモードは渡っていない

カーソルが当たっているときに正しくキューブが出てこない

なんのこっちゃですが、見た目がおかしい不具合あり
直した

配置時に配置モードを利用する

       // 対象キューブ表面を適当なブロックに
        var sideTypeA = SideType.Air;
        var sideTypeB = SideType.Air;
        switch (this.mode)
        {
            case Mode.Cube:
                sideTypeA = SideType.Frost;
                sideTypeB = SideType.Frost;
                break;
            case Mode.SideA:
                sideTypeA = SideType.Frost;
                break;
            case Mode.SideB:
                sideTypeB = SideType.Frost;
                break;
            default:
                break;
        }
        var chunkInt3 = this.cubeDataWorld.CubePositionToChunkInt3(this.cubeCenter);
        this.cubeDataWorld.SetCubeData(this.cubeCenter, chunkInt3, CubeCategoryType.Basic, this.preLocateCubeRotationType, sideTypeA, sideTypeB);
        Destroy(this.preLocateCubeObject);
        this.preLocateCubeObject = null;

これでいいのかな?

なんか不具合
メッシュオブジェクトの作り方に問題がありそう

そう、オフセットの問題だった、解決

ということでできたー!

小ゴール達成なので、次に進めます