simplestarの技術ブログ

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

CubeWalk:ホットバーUIの作成

前置き

だんだん作るものが具体的になってきました。
現在はキューブを配置するときのためにホットバーが必要です。
インベントリ操作は Inventory モードに入ったときで、そのときにはホットバーが不要
それ以外の Explore モードではホットバーが必要です。

ホットバーとは、インベントリ表示のときに現れる一段目の 0 ~ 8 スロットを指します。

こんなの想像してます。

f:id:simplestar_tech:20191229001917p:plain
ホットバーのイメージ

とりあえず複製

インベントリの 1段目ならば、それを複製してしまえばよいです。

入力モード切替で表示と非表示

実装から察するに、次のイベントを購読することになるはず
inputModeStateMachine.onChangeInputMode

よく見ていくと実は違っていて、すでに Inventory に関するクラスが、インベントリを開くときと閉じるときでイベントを発行しています。
これを購読することにしました。

            this.itemInventory.onOpenInventory += OnOpenInventory;
            this.itemInventory.onCloseInventory += OnCloseInventory;
        }

        private void OnOpenInventory()
        {
            this.panelHotbar.gameObject.SetActive(false);
        }

        private void OnCloseInventory()
        {
            this.panelHotbar.gameObject.SetActive(true);
        }

インベントリのスロット状態変更と連動

インベントリ側がスロット状態の変更をイベント発行してくれたら、これを購読したいなぁと思います。
そうすれば、ホットバーの内容も連動して切り替わっていけそう

もっと、簡略化方法を思いついた
ホットバー作成とインベントリ作成を連動しておいて、それぞれ panel を分けておく
データ上は一緒にするというもの

言葉ではどういったイメージなのか難しいのでコードにしてみるとこう!

        void Start()
        {
            // パネルに並ぶslot名からTransform配列を作成(Hotbar用)
            this.RetrieveSlotTransforms(this.panelHotbar);
            // パネルに並ぶslot名からTransform配列を作成(Inventory用)
            this.RetrieveSlotTransforms(this.panelInventory);
        }

        /// <summary>
        /// パネルに並ぶslot名からTransform配列を作成
        /// </summary>
        private void RetrieveSlotTransforms(Transform panelTransform)
        {
            foreach (Transform slotImage in panelTransform)
            {
                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); });
                        }
                    }
                }
            }
        }

これで完全同期となりました

アクティブなスロットを切り替えたい

いざ Minging モードでキューブを配置したとき、いったいどのキューブを置くのか?
そもそもキューブアイテムを選択していないときに置くという操作はできないはずだが

これを外部処理にどう伝えるのか

単にインベントリクラスにGetItemを作っておけば外部の人たちは困らないはず

        /// <summary>
        /// インベントリで現在アクティブとしているスロットのアイテム
        /// </summary>
        internal ViewItem ActiveSlotItem { get; private set; }

ところで、誰がどうやってこの ActiveSlotItem を決定するかだが…
Inventory クラスに行わせてしまいましょう。

ユーザの入力は?
マインクラフトだと確か
マウスホイールを回すと、画面最下部のホットバーでアイテムが選択されます。ツルハシと剣を持ち替えるときなどに使います。

1~9キー
各数字キーを押すと、ホットバー(アイテムスロット)のアイテムを選択できます。ホットバー左端のマスが1で、右端が9です

うーん、数字キーは今のところいらないかな
となると次の実装でクリア

            // マウスホイールでアクティブスロットを移動
            this.selectSlotAction.performed +=
            ctx =>
            {
                this.activeSlotIndex += (int)Mathf.Sign(ctx.ReadValue<float>());
                this.activeSlotIndex = this.activeSlotIndex % 9;
                if (0 > this.activeSlotIndex)
                {
                    this.activeSlotIndex += 9;
                }
            };

アクティブであることを表示するために画像を用意してこんなコードで見た目を調整

            // マウスホイールでアクティブスロットを移動
            this.selectSlotAction.performed +=
            ctx =>
            {
                this.activeSlotIndex -= (int)Mathf.Sign(ctx.ReadValue<float>());
                this.activeSlotIndex = this.activeSlotIndex % 9;
                if (0 > this.activeSlotIndex)
                {
                    this.activeSlotIndex += 9;
                }
                this.ChangeActiveSlot(this.activeSlotIndex);
            };


            // 初回起動時にデータは取得しておく
            this.OnOpenInventory();

            // ディフォルトのアクティブスロットの表示
            this.ChangeActiveSlot(this.activeSlotIndex);
        }

        /// <summary>
        /// アクティブスロット変更
        /// </summary>
        private void ChangeActiveSlot(int nextSlotIndex)
        {
            // 実際に表示している枠を移動
            this.imageActiveFrame.SetParent(this.slotTransforms[nextSlotIndex]);
            this.imageActiveFrame.localPosition = Vector3.zero;
            this.ActiveSlotItem = this.items[nextSlotIndex];
            // 外部にアクティブなアイテム変更を通知
            this.onChangeActiveSlot?.Invoke(this.ActiveSlotItem);
        }

これによって、こんなことができるようになりました。

まとめ

要するに見た目がこういうやつを紆余曲折しながら形にしたという話でした