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 をつくる
UIToolkit と uGUI という UI の作り方があります。
uGUI の作り方は、参考動画のように
- GameObject をヒエラルキー上で階層化する
- 各 GameObject に
Image
,CanvasRenderer
,VerticalLayoutGroup
など、 UI 用のコンポーネントをつける
が基本です。
この動画で初めてでてきましたが AutoLayout
を利用することで動的に配置を変更したい、垂直に UI を配置したい、という要件を満たせるようです。
というわけで、今回は AutoLayout を UI 構築に利用します。
UI で AutoLayout を利用する
AutoLayout について学びながら、動画を参考に UI を作成しました。 Unity uGUI の AutoLayout を調べる で調べてみましたが、難しいですね。 実際に自分でゲームを作るときにあらためて試行錯誤しながら作ることになりそうです。
ChoicePanel は下記画像のような構成になっています。
AlignmentGroup は「タイトルテキスト」「選択肢ボタン」を縦に並べつつ、中央の決まった領域に配置するためのオブジェクトです。
VerticalLayoutGroup
というコンポーネントがついており、 LayoutGroup
の 1 種であることがわかります。
これによって、子要素のうち LayoutElement
であるものをすべて取得しサイズを計算して、子要素の RectTransform を VerticalLayoutGroup が書き換えます。
子要素のサイズをそのまま利用したいため、 Control Child Size を true にしているという捉え方なのですがあっているのかな…
選択肢ボタンには LayoutElement
コンポーネントを付与しています。
これによって、 ChoiceButton
自体が望むサイズを設定できます。
ただし、このサイズがそのまま必ず叶えられるわけではないことに注意です。
親 GameObject にアタッチされている LayoutGroup
が計算したうえで、 ChoiceButton
の RectTransform が書き換えられます。
ChoiceButton の RectTransform をみると、 PosX, Y, Width, Height が書き換えられなくなっており、親 GameObject LayoutGroup に支配されることがわかりますね。
選択肢ボタン画像の引き伸ばしに対応する
ボタンに画像をつかうとき、ボタンサイズを大きくすると画像が引き伸ばされてデザインが崩れる可能性があります。
Image Type Sliced
を利用することで、四隅のサイズは固定し、それ以外の領域は中央の画像を複数繰り返して構成する、ということが可能なようです。
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();
}