lime雑記

ゲーム開発、その他雑記。

【Unity】LimeLibraryでUIを実装する : 実装編5

概要

自分用に作成したLimeLibraryを用いたUIの実装例についての続きになります。
前回はこちら。
limegame.hatenablog.com

このライブラリでどのようなことができるかについてはこちらの記事をご覧ください。
limegame.hatenablog.com

今回の内容

全体のフローを完成させます。

実装手順

アクションリスト画面とアイテム詳細画面の中身を実装

アイテムリスト画面と同様、アクションリストとアイテム詳細画面の中身を実装します。
また、今回アクションリストは固定の選択肢とします。

アクションリスト画面
ActionUiElements.cs
using System;
using LimeLibrary.Input;
using LimeLibrary.UI.Module.Selectable;
using LimeLibrary.UI.Module.Selectable.SelectableAppearance;
using LimeLibrary.UI.Module.SelectCursor;
using LimeLibrary.UI.Parts;
using LimeLibrary.UI.View;
using UniRx;
using UnityEngine;

namespace Sample
{
    public class ActionUiElements : MonoBehaviour
    {
        [SerializeField] 
        private UIButton _button;

        public IObservable<Unit> OnClickObservable => _button.OnClickObservable;
        
        public void SetupSelectable(IUIView parentView, SelectCursor selectCursor)
        {
            var selectableExtender = new SelectableExtender(parentView, _button.Button);

            var appearanceCursor = new CursorMover(gameObject, selectCursor);
            // ゲームパッド操作時はSelected時にカーソルを動かす
            selectableExtender.AddSelectableAppearance(ExtendSelectionState.Selected, appearanceCursor, InputMode.Gamepad);
            // マウス操作時はHighlighted時にカーソルを動かす
            selectableExtender.AddSelectableAppearance(ExtendSelectionState.Highlighted, appearanceCursor, InputMode.MouseKeyboard);
        }
    }
}

1つのアクションの選択肢に対するUiElmentsです。
アイテムリストの1アイテムと同様、クリックするためのボタン機能と選択時挙動のセットアップ機能を実装しています。

// ActionListUiElements.cs
using System;
using System.Collections.Generic;
using LimeLibrary.UI.Module.SelectCursor;
using LimeLibrary.UI.View;
using UniRx;
using UnityEngine;

namespace Sample
{
    public class ActionListUiElements : MonoBehaviour
    {
        [SerializeField]
        private List<ActionUiElements> _actionUiElementsList;
        [SerializeField] 
        private GameObject _cursorObject;

        private readonly Subject<int> _onClickSubject = new();
        public IObservable<int> OnClickObservable => _onClickSubject;

        public void Initialize(IUIView parentView)
        {
            // カーソルのセットアップ
            var selectCursor = new SelectCursor(parentView, _cursorObject);

            // アクション選択肢のセットアップ
            for (var i = 0; i < _actionUiElementsList.Count; i++)
            {
                var actionUiElements = _actionUiElementsList[i];
                actionUiElements.SetupSelectable(parentView, selectCursor);
                // クリック時、インデックス情報を足してイベント発行
                int index = i;
                actionUiElements.OnClickObservable.Subscribe(_ =>
                {
                    _onClickSubject.OnNext(index);
                }).AddTo(actionUiElements);
            }
        }
    }
}

アクションリストのUiElementsです。
カーソルのセットアップと、先ほどの1アクションのセットアップ、クリック時イベントにインデックス情報を追加してのイベント発行を行っています。

// ActionListView.cs
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using LimeLibrary.UI.View;
using UnityEngine;

namespace Sample
{
    public class ActionListView : UIView
    {
        [SerializeField]
        private ActionListUiElements _uiElements;

        public IObservable<int> OnClickObservable => _uiElements.OnClickObservable;
        
        protected override async UniTask OnInitialize(CancellationToken cancellationToken)
        {
            _uiElements.Initialize(this);
        }
    }
}

すでに作成済みのActionListViewクラスにはActionListUiElementsへの参照を追加し、Initializeとクリック時イベントの伝播を追加します。

アイテム詳細画面
// ItemInfoUiElements.cs
using LimeLibrary.UI.Parts;
using UnityEngine;

namespace Sample
{
    public class ItemInfoUiElements : MonoBehaviour
    {
        [SerializeField] 
        private UIText _nameText;
        [SerializeField]
        private UIText _detailText;

        public void SetItemName(string itemName)
        {
            _nameText.SetText(itemName);
        }

        public void SetItemDetail(string itemDetail)
        {
            _detailText.SetText(itemDetail);
        }
    }
}

アイテム情報画面のUiElmentsです。
アイテム名とアイテム詳細のテキストをセットする機能のみ持たせています。

// ItemInfoView.cs
using System.Threading;
using Cysharp.Threading.Tasks;
using LimeLibrary.UI.View;
using UnityEngine;

namespace Sample
{
    public class ItemInfoView : UIView
    {
        [SerializeField]
        private ItemInfoUiElements _uiElements;
        
        protected override async UniTask OnInitialize(CancellationToken cancellationToken) { }

        public void SetItemData(ItemData itemData)
        {
            _uiElements.SetItemName(itemData.Name);
            _uiElements.SetItemDetail(itemData.Detail);
        }
    }
}

すでに作成済みのItemInfoViewクラスにItemInfoUiElementsの参照を持たせ、ItemDataのテキストデータをItemInfoUiElementsに流す関数を追加します。
(ItemDataに詳細テキストであるDetailプロパティを追加しています)

プレハブにアタッチ

上記のクラスが作成出来たらプレハブの方にアタッチして参照のセットを行います。

ActionListUiElements
ActionUiElements
ItemInfoUiElements

フローに組み込む

アクションリストの表示→選択のフロー

まずはアクションリストをフローに組み込みます。

// InventoryFlowState.cs
namespace Sample
{
    public enum InventoryFlowState
    {
        SelectItem,
        // 追加
        SelectAction,
    }
}

アクション選択のステートを追加するため、InventoryFlowStateにSelectActionの定義を追加します。

// ItemActionResult.cs
namespace Sample
{
    public enum ItemActionResult
    {
        None,
        Use,
        Throw,
        Put,
    }
}
// ItemSelectResult.cs
namespace Sample
{
    public class ItemSelectResult
    {
        public ItemData SelectItemData { get; set; }
        public ItemActionResult? ItemActionResult { get; set; }
    }
}
// InventoryFlowContext.cs
using System.Collections.Generic;
using LimeLibrary.UI;

namespace Sample
{
    public class InventoryFlowContext : UIAppFlowContext
    {
        public IReadOnlyList<ItemData> ItemDataList { get; private set; }
        // 追加
        public ItemSelectResult ItemSelectResult { get; } = new();

        public void SetItemDataList(IReadOnlyList<ItemData> itemDataList)
        {
            ItemDataList = itemDataList;
        }
    }
}

アクションの結果を返すために、ItemSelectResultクラスと結果のenum定義を追加しコンテキストにデータを追加します。

// InventoryApp.cs
public class InventoryApp : UIApp
{
    // 省略

    public async UniTask<ItemSelectResult> WaitItemActionResult(CancellationToken cancellationToke
    {
        await UniTask.WaitUntil(() => _flow.Context.ItemSelectResult.ItemActionResult.HasValue,
            cancellationToken: cancellationToken);
        return _flow.Context.ItemSelectResult;
    }
}

また、App側に結果を返すための関数を追加しています。
(今回はUniTaskで結果を待てるようにしています)

// InventoryFlowSelectAction.cs
using Cysharp.Threading.Tasks;
using LimeLibrary.UI;

namespace Sample
{
    public class InventoryFlowSelectAction : UIAppFlowState<InventoryFlowState, InventoryFlowContext>
    {
        public override void Initialize() { }

        public override async UniTask<InventoryFlowState> Execute()
        {
            var actionListView = GetView<ActionListView>();
            
            // ActionListViewの表示
            await actionListView.Show(CancellationToken);
            
            // キャンセルキーが押されるかアクションがクリックされるまで待機
            var result = await UniTask.WhenAny(
                    Context.CancelInputReceiver.OnInputObservable.ToUniTask(true, cancellationToken: CancellationToken),
                    actionListView.OnClickObservable.ToUniTask(true, cancellationToken: CancellationToken));

            switch (result.winArgumentIndex)
            {
                case 0:
                    // キャンセル
                    // アクションリスト画面を閉じてアイテム選択に戻る
                    await actionListView.Hide(CancellationToken);
                    return InventoryFlowState.SelectItem;
                case 1:
                    // アクションがクリックされた
                    // "やめる"がクリックされたならアクションリスト画面を閉じてアイテム選択に戻る
                    if (result.result2 == 3)
                    {
                        await actionListView.Hide(CancellationToken);
                        return InventoryFlowState.SelectItem;
                    }
                    else
                    {
                        Context.ItemActionResult = result.result2 switch
                        {
                            0 => ItemActionResult.Use,
                            1 => ItemActionResult.Throw,
                            2 => ItemActionResult.Put,
                            _ => ItemActionResult.None
                        };
                        RequestEnd();
                        break;
                    }
            }
            
            return InventoryFlowState.SelectAction;
        }
    }
}

アクション選択ステートを追加します。
アイテム選択リストと同様、キャンセルキーかアクション選択かで分岐を行い、キャンセルもしくはやめる選択ならアイテムステートに戻る、アクション選択ならコンテキストに結果を代入しUIを閉じる挙動になっています。

// InventoryFlow.cs
public class InventoryFlow : UIAppFlow<InventoryFlowState, InventoryFlowContext>
{
    public InventoryFlow(UIApp uiApp) : base(uiApp)
    {
        // ステートを登録
        AddState<InventoryFlowSelectItem>(InventoryFlowState.SelectItem);
        // 追加
        AddState<InventoryFlowSelectAction>(InventoryFlowState.SelectAction);
    }

    // 省略
}

InventoryFlowにも先ほど作成したInventoryFlowSelectActionのステートを追加します。

アイテム詳細画面表示処理

アイテム詳細画面の表示処理をフロー内に記述します。

// InventoryFlowSelectItem.cs
using Cysharp.Threading.Tasks;
using LimeLibrary.UI;

namespace Sample
{
    public class InventoryFlowSelectItem : UIAppFlowState<InventoryFlowState, InventoryFlowContext>
    {
        public override void Initialize() { }

        public override async UniTask<InventoryFlowState> Execute()
        {
            var itemListView = GetView<ItemListView>();
            
            // ItemListの作成
            itemListView.CreateList(Context.ItemDataList);

            // 追加
            // Select時とHighlightedにアイテム詳細画面更新
            var disposable = Observable.Merge(
                    itemListView.OnHighlightedObservable,
                    itemListView.OnSelectObservable
                ).Subscribe(index =>
            {
                var itemInfoView = GetView<ItemInfoView>();
                itemInfoView.SetItemData(Context.ItemDataList[index]);
                itemInfoView.Show(default).RunHandlingError().Forget();
            });
            
            // ItemListViewの表示
            await itemListView.Show(CancellationToken);
            
            // キャンセルキーが押されるかアイテムがクリックされるまで待機
            var result = await UniTask.WhenAny(
                    Context.CancelInputReceiver.OnInputObservable.ToUniTask(true, cancellationToken: CancellationToken),
                    itemListView.OnClickObservable.ToUniTask(true, cancellationToken: CancellationToken));

            // 追加
            disposable.Dispose();

            switch (result.winArgumentIndex)
            {
                case 0:
                    // キャンセル
                    RequestEnd();
                    break;
                case 1:
                    // アイテムがクリックされた
                    // 変更
                    int itemDataIndex = result.result2;
                    Context.ItemSelectResult.SelectItemData = Context.ItemDataList[itemDataIndex];
                    return InventoryFlowState.SelectAction;
            }
            
            return InventoryFlowState.SelectItem;
        }
    }
}

InventoryFlowSelectItemのリスト生成の後でアイテム詳細画面の更新処理を追加します。
また、InventoryFlowSelectItemのアイテムクリック時の挙動を、選択したItemDataを結果クラスに格納しInventoryFlowState.SelectActionを返すように変更しておきます。
ActionListViewの表示はSelectActionのステート側で行うため、こちらでは不要になります。

動作確認

アイテム選択とアクション選択のフロー遷移と、アイテム詳細画面の更新が行われれば成功です。

次回

作成中…