見出し画像

Godotエンジン Layoutメモ

ControlのLayoutわからん。難しいですよね?
エディタのGUIから設定する分にはいいんだけど、スクリプトからいじろうとすると難易度が上がります。いちユーザ(ゲーム制作者)が全部の機能を把握する必要はないんだけど、ある程度は理解しておいたほうが気持ちよく作れるので、テストコード書いたりソースコード呼んだりして、いろいろ分かったことをこの記事に書き連ねます。
最終更新: 2023/09/17


■Layoutが難しい理由

  • エディタのインスペクタGUIが「いい感じに」やってくれているので、逆に内部動作が想像しにくい。

  • 関数が豊富でいろんな関数で同じ内容を設定できる。どれが低レベルアクセスで、どれが便利関数なのかわかりにくい。

  • それぞれの関数の副作用が強く、動作が把握しにくい。

  • エディタ内ドキュメントに記載されている関数/プロパティと、実際に利用できる関数/プロパティに差がある。GDからのアクセスがある程度隠ぺいされている(実際は使える)。

  • 一部enumの名前に惑わされることがある。

■先にまとめ

エディタのインスペクター上では、Layout ModeやAnchors Presetを設定できるが、あれを設定するのと同じことをスクリプトから処理することはできない。スクリプトの関数が、GUI操作と一対一に対応していない。
さらにそもそも、Anchors Presetなどは状態として保持されているのではない。Presetはアクセスの手段だったり、現在の状態から逆算して得られる通知でしかないのだ。(例えば矩形が親矩形の左上に配置されていると判断されれば、エディタ上のPresetは「左上」が表示される。)
なので固執しても仕方がない。

Layoutによるレイアウトは、内部値(C++)のanchor: float[2]とoffset: float[2]で決まる。そのためのローレベルインターフェースが、anchor / offset / position / size の4つの概念である。
繰り返すが、Presetは状態ではない。

次の4つの関数を使えば、やりたいことはほぼほぼカバーできる。
・set_anchor (ただし、引数 keep_offset = true)
・set_offset
・set_position (ただし、引数 keep_offset = false)
・set_size (ただし、引数 keep_offsets = false)
この4つがクセのない低レベルアクセスである。anchorかpositoinで位置を決めて、offsetかsizeで大きさを決める。(anchor / positoin)×(offset / size)、どの組み合わせを使うかは、ゲーム全体やUIパーツ単位で方針を決めておく。プロパティによる値のセット(anchor_left等)は、副作用をコントロールできないのでやらないほうがいい。
その上で、これらの関数を自分なりにラッピングすればいい。例えば筆者は、anchorは上中下×左央右の組み合わせ9か所使えれば十分なので、enumで指定できるようにした。

名前にpresetが付いている関数はどれも副作用が強い。その中でもset_anchors_presetだけは素直に使える。set_anchors_presetとset_sizeだけで乗り切るのもありなのでは。

■anchor/offsetとposition/sizeの関係

anchor値(0.0~1.0)とoffset値(ピクセル数)が、Controlに保存されている実体、C++上のメンバ変数。positionとsizeは、anchorとoffsetから二次的に決まる。anchorかoffsetが変わると、positionとsizeが再計算され、結果がキャッシュされる。
positionやsizeに指定値をセットする関数やプロパティにおいては、anchorとoffsetが「逆算」される形になる。
ユーザーはこの関係を意識せずに使えるようにできてるんだけど、その事実を知ってないと安心できないよね。

■プロパティとメソッド

プロパティと関数は一対一か

例えば、sizeを設定したい場合、size = Vector2(1.0, 1.0)とset_size(Vector2(1.0, 1.0), false)の2通りの方法がある。sizeだけでなく、anchorもoffsetもpositionも同様にプロパティ経由と関数経由がある。ソースコードで確認する限り、これはどちらから呼んでも、同じC++関数が呼ばれるので結果は同じである。プロパティの場合は、オプション引数はデフォルト値が用いられる。

set○○関数は、○○プロパティへのsetと同じである、これがすべてのプロパティに言えるなら話が単純なんだが・・・。

例外もある

set_anchors_presetという関数がある。これは、指定LayoutPresetに応じて、上下左右にset_anchorをする、分かりやすい関数である。文字通り「アンカー」を「プリセット」を使って「セット」したければ、この関数を使えばいい。

一方で、anchors_presetというプロパティが存在する。これはエディタ上のドキュメントには載っていないが、一応使うことができる。これに対するセットはset_anchors_preset関数を呼ぶのと同じなのか?結果は違う。anchors_presetプロパティへのセットは、_set_anchors_layout_preset関数を呼ぶのと等しい。_set_anchors_layout_presetは、anchors_presetプロパティ同様、ドキュメントからは隠されているが、使用可能な関数だ。機能は・・・ソースコードを読んでもよくわからん。
一つ言えることは、Godotエディタが自身のGUIのために使っているControl :: _set_anchors_layout_preset関数を、何らかの理由でGDからも「一応」使えるようにしているものだということだ。

運用方針

エディタ内ドキュメントに無いプロパティや関数は使わない方がよさそう。使わなくともLayoutは、それで十分設定可能である。後述のlayout_modeに関しては、公開範囲の手段ではアクセスできないのだが、アクセスしなくても制作に何の問題無い。むしろ触らない方がいい。

■LayoutMode

エディタのインペクタ上では、LayoutModeが設定できる。これをPositionに設定すると、アンカーが左上に固定され、PositionとSizeしかGUIでは設定できない単純なモードになる。パラメータ数を制限することで取っつきやすくするための機能なのだろう。
これはスクリプトから普通には設定できない。厳密にはできる。エディタ上のドキュメントに公開されていないだけで、layout_modeプロパティや、_set_layout_mode関数が使用可能である。しかしソースコードを荒く読む限り、設定したところで、ゲームに影響しないように思う。本当に単にエディタのGUIを制限するだけの機能のようだ。

■[基本1]set_anchor関数

指定方向(上下左右)にアンカー値(普通は0.0~1.0)がセットされる。
anchor_left、anchor_right、anchor_top、anchor_bottom各プロパティへのセットは、この関数をオプション引数デフォルトで呼ぶのと等しい。

引数keep_offset

オプション引数 keep_offset [デフォルト: false]
trueの場合、アンカーだけが動く。
falseの場合、アンカーの移動を打ち消すように、指定方向のオフセットが連動して動く。矩形の見た目の位置が変わらない。さらにこのとき、push_opposite_anchor==trueならば、逆サイドのオフセットも連動して動く。

正直、デフォルト値のfalseは、コードから設定するには非直感的で、あまり嬉しくない。プロパティではなく、set_anchor関数を用いて、常にtrueで運用したい。
もしくは、プロパティを使うにしても、アンカーとオフセットを必ず同時に操作する運用にするといいかもしれない。

# 関数ではなくプロパティを使う運用
# アンカーとオフセットのプロパティは、必ずanchor->offsetの順に同時にセットする。
# これなら「0.5にアンカーがあって、そこから32px左が矩形の左端」だと、ぱっと見でわかる。
control.anchor_left = 0.5 ←ここでoffsetが変わってしまう
control.offset_left = -32.0 ←でも直後にoffsetが再設定されるからOK

引数push_opposite_anchor

オプション引数 push_opposite_anchor [デフォルト: true]
セットした側のアンカーが逆サイドのアンカーをまたいで超えてしまい、上下左右の関係が崩れるような際の、正規化動作に影響する。例えばleftがrightよりも大きくなった場合の処理だ。
trueならば、逆サイドのアンカーもセット値(つまり同値)にすることで解決する。
falseならば、指定サイドのアンカーを逆サイドのアンカー値でクランプする。(これも結果的に同値になる)

■[基本2]set_offset関数

指定方向(上下左右)にオフセット値(ピクセル数)がセットされる。
offset_left、offset_right、offset_top、offset_bottom各プロパティへのセットは、この関数を呼ぶのと等しい。

■[基本3]set_position関数

positionが指定値(Vector2)になるよう、逆計算してanchorかoffsetのどちらかを再設定する。anchorとoffsetどちらを動かすかは引数で決まる。

引数keep_offsets

オプション引数 keep_offsets[デフォルト: false]
falseの場合は、アンカーを維持してオフセットが動く。trueの場合はオフセットを維持してアンカーが動く。用途を考えれば、デフォルトのfalseが直観的なように思う。

■[基本4]set_size関数

sizeが指定値(Vector2)になるよう、逆計算してanchorかoffsetのどちらかを再設定する。anchorとoffsetどちらを動かすかは引数で決まる。
指定サイズが、後述のcombined_minimum_sizeを下回る際は、最小サイズでクランプされる。

引数keep_offsets

オプション引数 keep_offsets[デフォルト: false]
falseの場合は、アンカーを維持してオフセットが動く。trueの場合はオフセットを維持してアンカーが動く。用途を考えれば、デフォルトのfalseが直観的なように思う。

■combined_minimum_size

combined_minimum_sizeは、custom_minimum_sizeとminimum_sizeに対して、縦横別々にmaxを取ったサイズである。
combined.x = max (custom.x, minimum.x)
combined.y = max (custom.y, minimum.y)
これは、前述のset_sizeの他、様々なところで参照され、Controlに対する諸操作の結果、矩形がこのサイズを下回らないように、常に制御されている。

custom_minimum_size

custom_minimum_sizeは同名のプロパティやset_custom_minimum_size関数でセット可能である。

minimum_size

minimum_sizeは_get_minimum_size関数で取得できるサイズである。_get_minimum_size関数は、Controlにおいては常にVector2.ZEROを返す。では何の意味があるのかというと、継承先でオーバーライドして使うものである。実際、TextureRectノードはこれをオーバーライドしており、設定されたExpand Modeとテクスチャサイズに応じて適切な値を返すようになっている。
ユーザがControl派生クラスを作る時にも、これを同様にオーバーライドして使えるだろう。

■[応用1]set_anchors_preset関数

前述の通り、指定LayoutPresetに応じて、上下左右にset_anchorをする関数。set_anchorに用いるkeep_offset引数を指定でき、デフォルトでfalseである。用途を考慮すると、アンカーを動かすために呼ぶと思うので、常にtrueで運用したい。
CustomはLayoutPresetの範囲に無いので、指定できない。(指定しても意味が無いが。)

■[応用2]set_offsets_preset関数

指定LayoutPresetに応じて、上下左右のオフセットを変更する関数。4回set_anchorするだけのset_anchors_presetと違い、もう少し複雑である。
詳細は追いきれないが、そもそもLayoutPresetに合わせてオフセットを変えること自体がマニアックな動作であり、この関数自体は忘れてしまっていいように思う。特殊な要望に応えるための関数といった感じがする。

■[応用3]set_anchor_and_offset関数

set_anchorとset_offsetを順次呼ぶだけの関数。
set_anchorの引数keep_offsetは指定できず、false固定である。が、そもそも直後にset_offsetを呼ぶのだから、trueでもfalseでも結果は同じである。

■[応用4]set_anchors_and_offsets_preset関数

set_anchors_presetとset_offsets_presetを順次呼ぶ関数。どういうシチュエーションで使うのを想定しているのか、用途がわからない。
どうでもいいけど、anchorとoffsetが複数形なので、記述を間違えて「わからんバグ」にならないよう気を付けたい。

■enum LayoutPreset

インスペクター上の"Anchors Preset"項目で選択できる内容。Top Left、Right Wide、V Center Wideなど。関数名でもAnchorsPresetと呼ばれることがあり、命名が適切ではないのではないか、と最初は思う。しかし、実際は、アンカーをセットするだけでなく、アンカーを固定したままオフセットを動かすためにもこのenumが使われており、"AnchorsPreset"という名前にするわけにもいかないのも、うなずける。
項目は、インスペクター上の選択肢とほぼイコールだが、GUI上で選択できるCustomがenum値として存在しない。実装としては-1で代替されている。
LayoutPresetは関数に引数として用いられるが、メンバ変数として保持されるわけではなく、処理の分岐に用いられるだけで、値は捨てられる。現在のLayoutPresetを返す関数があるが、これは、アンカー値が特定の値(0.0/0.5/1.0)の組み合わせの時に、適切なenum値を返すという仕組みになっている。

■enum LayoutPresetMode

LayoutPresetによるアンカー変更時の挙動を決定するenum。
GD上でもC++上でも、このenumの変数名はresize_modeであり、こちらの名前の方がピンと来る。MINSIZE、KEEP_WIDTH、KEEP_HEIGHT、KEEP_SIZEの4種類。LayoutPresetを指定してアンカーを変えるタイプの関数で、LayoutPresetと共に引数として使われる。具体的には、set_anchors_and_offsets_presetとset_offsets_preset。条件分岐に使われ、特に保持されない。
アンカーをストレッチ系(Left Wide等)から非ストレッチ系(To Left等)に変えた時に、伸びてたサイズを最小値に戻すかどうかを決める。多分。

■Anchors Preset "Custom"

エディタのインスペクタ上のように、スクリプトからもControlをCostom状態にしたくなる。でも意味が無い。そもそもLayoutPresetは、アンカーやオフセットの変更方法を指定するためのものであって、状態ではない。LayoutMode同様、エディタの動作を切り替えるために保持しているフラグだ。スクリプトからは、自由にset_anchorすればいい。
またそもそも、set_anchors_preset関数などは、引数がenum指定であり、Customを意味する-1など代入することはできない。anchors_presetプロパティに-1をセットすることならできるが、前述の通り、エディタ上のドキュメントに公開されているプロパティではないので、使わない方がいいだろう。

■画面解像度

プロジェクト設定>表示>ウィンドウから次の項目を設定する。

  • サイズ>ビューポートの幅: 1920など

  • サイズ>ビューポートの高さ: 1080など

  • ストレッチ>モード: canvas_item

  • ストレッチ>アスペクト: expand

  • ストレッチ>スケール: 1

これでいい。少なくとも3Dのゲームはこれで理想的に動く。アス比に応じて黒帯を出したかったり、ドットバイドットにしたかったりする場合はまた別の設定が必要だけど、3Dの画面の手前に2DのUIが表示されるようなマルチプラットフォームのゲームはこの設定。

「サイズ」はUIの仮想最小解像度で、この解像度上で自分のゲームのUIを、互いに重なったり、はみ出したりしないように配置する。もちろんアンカーは画面端や中央を基準に、適切に設置する。これだけで、解像度が変わっても、Controlが追従して拡縮するようになる。4Kにも720にも、ワイドモニタにも、縦長ウィンドウにも対応する。例え解像度が動的に変更されても問題無い。

  • (実解像度÷仮想解像度)のスケーリングが常に画面全体のUIにかかる

  • 縦長になった場合→横最小(1920)を維持して、縦の仮想解像度が伸びる。

  • 横長になった場合→縦最小(1080)を維持して、横の仮想解像度が伸びる。

「仮想解像度」というのは自分の勝手な解釈だが、だいたいあってる気がする。

■GrowDirection

Labelノードは、clip_textがfalseの時は、表示内容textに応じてsizeが自動的に変更される。
エディタのGUIからLayoutを設定するときは、AnchorsPresetの設定値に応じて、伸長される方向が決定される。例えば、Top Rightに設定している場合は、左下に伸びていく。しかしスクリプトで動的にLabelを生成する場合、AnchorsPresetは普通設定しないので、そのままだと常に右下に伸長してしまう。
そこで、Control.grow_horizontalとControl.grow_verticalを変更することで、右下以外にも伸ばすことができる。例えばgrow_horizontalをGROW_DIRECTION_BOTHにすれば、常にセンタリングされる。


この記事が気に入ったらサポートをしてみませんか?