📌

Godot EngineでContainerにButton機能を持たせる

2023/08/14に公開

はじめに

ボタンは単一のテキストや画像だけでなく複数の情報をまとめてボタンとして扱いたいときがあります。例えば装備名 + 値段、アイテム名 + 個数、スキル名 + 消費MPなど。Godot Engineの場合、これらの表示自体はHBoxContainer内にLabelを二つ入れるなどの方法で作ることができます。

アイテム名 + 個数の場合なら、以下のような構造になります。

  • ItemView(HBoxContainer)
    • ItemName(Label)
    • ItemCount(Label)

これをButtonノードでラップすればボタン機能が付与できますが、ButtonはContainerではないのでItemViewのサイズを考慮してくれません。だから、ItemViewのデザインに応じて適切なサイズをcustom_minimum_sizeとかで手動で指定しないといけません。これは少々面倒です。

Containerなら子要素のサイズ分広がってくれるので手動調整が不要になります。でもContainerはButtonではない。→ ContainerとButtonが合わさった機能が欲しい、となります。

ContainerにButtonを埋め込む

結論としてPanelContainerの下にButtonを配置すればButton機能を持ったContainerを作れます。
先ほどのアイテム用のビューの場合、以下のような構造になります。

  • PanelContainer
    • ItemView (HBoxContainer)
      • ItemName(Label)
      • ItemCount(Label)
    • Button

Buttonノードを最後尾に置くことで、マウスの入力を受け取れます。ItemViewとButtonの順番を入れ替えると、ItemViewがマウス入力を取ってしまうのでホバーとかクリックとかが反応しなくなります。

これはアイテム用のビューなのでItemViewを配置していますが、他のスキル名 + 消費MPとか任意のノード(シーン)も同じように扱えます。

スキルの場合、

  • PanelContainer
    • SkillView ← スキル表示用のビュー(内部構造は省略)
    • Button

装備の場合、

  • PanelContainer
    • EquipmentView ← 装備表示用のビュー(内部構造は省略)
    • Button

ViewButtonとしてノード化する

PanelContainerとButtonの部分は同じなのでこれをノード化すると便利です。任意のビューを子要素に持てるボタンということでViewButtonという名前にします。

view_button.gd
class_name ViewButton
extends PanelContainer

var button: Button

func _init() -> void:
    button = Button.new()
    button.show_behind_parent = true
    add_child(button, false, Node.INTERNAL_MODE_BACK)

PanelContainerを継承してコンストラクタ内で内部にButtonを追加しています。add_childの3つ目の引数にINTERNAL_MODE_FRONTまたはINTERNAL_MODE_BACKを指定すると、追加したノードは内部ノードとして扱われ、get_children()やget_child_count()などのメソッドで無視されるようになります(これらのメソッドには内部ノードを含むようにするオプションもあります)。 
また、INTERNAL_MODE_BACKの場合は、最後尾(内部ノードではないノードの後)に配置されるようになるのでボタンがマウス入力を受け取れます(INTERNAL_MODE_FRONTの場合は最前列になります)。 
ただし、ボタンを最後尾に置くとボタンの描画が一番手前になるので、styleboxなどで色を付けると、肝心のViewが隠れてしまいます。そこで、button.show_behind_parent = trueとすることでボタンの描画を最奥にしています。

これで任意の子要素を持つことができるViewButtonノードができました。

  • ViewButton
    • ItemView

とか、

  • ViewButton
    • SkillView

のように任意の要素に子要素に入れてボタン化できます。

ただし、button押下のコールバック処理を登録するにはview_button.buttonのように内部のbuttonにアクセスする必要があることに注意してください。

view_button.button.pressed.connect(func():
    print("押された!")
)

Discussion