UnityNovelProj Unity で VisualNovel を作成する

url: https://www.youtube.com/watch?v=7fqpWLYlXj8&list=PLGSox0FgA5B58Ki4t4VqAPDycEpmkBd0i&index=66
title: "Make a Visual Novel in Unity 2023 - Episode 20 (part1) Designing The Choice Panel"
description: "Let's design a panel to offer choices to the playerButton Graphic: https://drive.google.com/file/d/1H91897WbXtSSPGH4FDBEhSxRQes2q_Tw/view?usp=drive_linkOther..."
host: www.youtube.com
favicon: https://www.youtube.com/s/desktop/0aaf30d6/img/favicon_32x32.png
image: https://i.ytimg.com/vi/7fqpWLYlXj8/maxresdefault.jpg

選択肢用の UI をつくる

UIToolkituGUI という UI の作り方があります。

uGUI の作り方は、参考動画のように

  • GameObject をヒエラルキー上で階層化する
  • 各 GameObject に Image, CanvasRenderer, VerticalLayoutGroup など、 UI 用のコンポーネントをつける

が基本です。

この動画で初めてでてきましたが AutoLayout を利用することで動的に配置を変更したい、垂直に UI を配置したい、という要件を満たせるようです。 というわけで、今回は AutoLayout を UI 構築に利用します。

Imgur

UI で AutoLayout を利用する

Unity uGUI の AutoLayout を調べる

AutoLayout について学びながら、動画を参考に UI を作成しました。 Unity uGUI の AutoLayout を調べる で調べてみましたが、難しいですね。 実際に自分でゲームを作るときにあらためて試行錯誤しながら作ることになりそうです。

ChoicePanel は下記画像のような構成になっています。

Imgur

AlignmentGroup は「タイトルテキスト」「選択肢ボタン」を縦に並べつつ、中央の決まった領域に配置するためのオブジェクトです。

VerticalLayoutGroup というコンポーネントがついており、 LayoutGroup の 1 種であることがわかります。 これによって、子要素のうち LayoutElement であるものをすべて取得しサイズを計算して、子要素の RectTransform を VerticalLayoutGroup が書き換えます。

子要素のサイズをそのまま利用したいため、 Control Child Size を true にしているという捉え方なのですがあっているのかな…

Imgur

選択肢ボタンには LayoutElement コンポーネントを付与しています。 これによって、 ChoiceButton 自体が望むサイズを設定できます。 ただし、このサイズがそのまま必ず叶えられるわけではないことに注意です。 親 GameObject にアタッチされている LayoutGroup が計算したうえで、 ChoiceButton の RectTransform が書き換えられます。

ChoiceButton の RectTransform をみると、 PosX, Y, Width, Height が書き換えられなくなっており、親 GameObject LayoutGroup に支配されることがわかりますね。

Imgur

選択肢ボタン画像の引き伸ばしに対応する

ボタンに画像をつかうとき、ボタンサイズを大きくすると画像が引き伸ばされてデザインが崩れる可能性があります。

Imgur

Image Type Sliced を利用することで、四隅のサイズは固定し、それ以外の領域は中央の画像を複数繰り返して構成する、ということが可能なようです。

Imgur

url: https://hiyotama.hatenablog.com/entry/2015/06/25/090000
title: "【Unity開発】uGUI Imageの使い方 基本からImageTypeの使い方やFill Methodの紹介など - Unity(C#)初心者・入門者向けチュートリアル ひよこのたまご"
description: "【更新】Unity 2021.1.0f1 Personal(2021年4月) Unity5.1.1f1 Personal(2015年6月)今回はuGUI ImageのImage Typeについて解説致します。 こちらの記事の続きです。 hiyotama.hatenablog.comImage Typeを活用するとメッセージウインドウやゲージなどが扱えるようになります。"
host: hiyotama.hatenablog.com
favicon: https://hiyotama.hatenablog.com/icon/link
image: https://cdn.image.st-hatena.com/image/scale/2fbde67daf5f86f059cd1fe5ba85cb8a3fc93ae4/backend=imagemagick;version=1;width=1300/http%3A%2F%2Fcdn-ak.f.st-hatena.com%2Fimages%2Ffotolife%2Fh%2Fhiyotama%2F20150624%2F20150624205139.png

選択肢パネルを実際に表示してユーザに選択させる

url: https://github.com/ganyariya/gnovel/pull/22
title: "20-2: 選択肢を画面に出して選択できるようにする by ganyariya · Pull Request #22 · ganyariya/gnovel"
description: "ChoicePanel に 仮ボタンを複数表示するところまで実装するButton がクリックされたらその index を記録して Panel を隠すrefactor: ChoicePanel に必要な doc をつけるbutton Width を描画するテキストから計算して適応し、ボタンサイズを自動調整するChoicePanel テストで選んだ選択肢を画面に出すようにするSum..."
host: github.com
favicon: https://github.githubassets.com/favicons/favicon.svg
image: https://opengraph.githubassets.com/1d2ee70d8165c26d9ed9ea086b695c8d1e2cb2e4961ded1ff02792f617006606/ganyariya/gnovel/pull/22

ChoicePanel ですが、 InputPanel と同じ仕組みで動いています。

  • ChoicePanel 自体は MonoBehaviour スクリプトでありChoicePanelManager という空の GameObject にコンポーネントとしてアタッチされて作動する
  • ChoicePanel.cs は VerticalLayoutGroup コンポーネントや TMPro Text コンポーネントを変数として持ち、 Show メソッドが呼ばれたときにこれらコンポーネントを編集することで選択肢を表示する
    • ChoicePanel コンポーネントが、UI 系コンポーネントを参照して値を変更する

Show メソッド内で GenerateChoices メソッドを読んで選択肢を生成します。 このとき、一度生成した ChoiceButton の GameObject は cachedChoiceButtons で管理することで、生成コストを下げておきます。

選択肢ボタンの onClick にコールバックを選択し、 ChoiceAnswer = 該当の index 選択肢を選択したことを表現します。 ここで、コールバック実装時に i を一度 index に移し替えています。 #closure を利用しており、 i をそのままキャプチャすると、 i が for で上がりきった値が表示されるためです。

 
        private void GenerateChoices(string[] choices)
        {
            float maxWidth = 0;
 
            var FetchButton = new Func<int, ChoiceButton>(i =>
            {
                if (i < _cachedChoiceButtons.Count) return _cachedChoiceButtons[i];
 
                var go = Instantiate(_buttonPrefab, _buttonLayoutGroup.transform);
                var choiceButton = new ChoiceButton
                {
                    button = go.GetComponent<Button>(),
                    text = go.GetComponentInChildren<TextMeshProUGUI>(),
                    layoutElement = go.GetComponent<LayoutElement>()
                };
                _cachedChoiceButtons.Add(choiceButton);
                return choiceButton;
            });
 
            for (var i = 0; i < choices.Length; i++)
            {
                var choiceButton = FetchButton(i);
 
                choiceButton.text.text = choices[i];
                choiceButton.button.onClick.RemoveAllListeners();
                // button がクリックされたらその index をもとに lastDecision を更新する
                // i をそのまま渡すと Closure の問題で length - 1 の値になってしまうため注意する
                var index = i;
                choiceButton.button.onClick.AddListener(() => ChoiceAnswer(index));
 
                var buttonWidth = Mathf.Clamp(
                    // TextMeshPro は該当のテキストを描画するために必要な Width (preferredWidth) を自動計算してくれる
                    BUTTON_PADDING_WIDTH + choiceButton.text.preferredWidth + BUTTON_PADDING_WIDTH,
                    MINIMUM_BUTTON_WIDTH,
                    MAXIMUM_BUTTON_WIDTH
                );
                maxWidth = Mathf.Max(maxWidth, buttonWidth);
            }
 
            foreach (var choiceButton in _cachedChoiceButtons)
                // layoutElement を操作することで AutoLayout 時における Button などの UI サイズを動的に変更できる
                // Button の Image Type を Sliced にしているため、Button のサイズを変更したとしても正しく画像が適応される
                choiceButton.layoutElement.preferredWidth = maxWidth;
 
            for (var i = 0; i < _cachedChoiceButtons.Count; i++)
                _cachedChoiceButtons[i].button.gameObject.SetActive(i < choices.Length);
        }
 
        private void ChoiceAnswer(int index)
        {
            LastDecision.Answer(index);
            Hide();
        }