🦜

Go&EbitengineでGUIを設計する

に公開

はじめに

雑なボタン程度なら簡単だが、それらをグループ化してパネルに乗っけて、ってなると途端に難しくなるのがGUIというもの。階層構造を表現し、制御する必要が出てくるからだ。
オブジェクト指向言語ではどう転んでもCompositパターン的なものになるわけだが、Goはオブジェクト指向言語ではないので改めて考える必要がある。
今回は最近作ってるサンプルのGUI周りを一新してメニュー画面を実装してみたのでそのお話。

Githubに置く

実行はこちら。
https://mirichi.github.io/EbitenSampleTest/005/
コードはこちら。
https://github.com/mirichi/EbitenSampleTest/tree/main/005

これは何か

この記事でボタンを作ってみたが、いかにも雑な感じだったので、きちんと制御できるGUIシステムとして考え直してみた。
https://zenn.dev/mirichi/articles/f33b71dd08b8b9
まだ粗削りなわけだが、Goでこういった類のものを実装する参考にも反面教師にもなるだろう。

内容

Controlインターフェース

現状、Button、Label、Menuを実装している。これらをControlと総称することにして、Controlインターフェースを作っている。
https://github.com/mirichi/EbitenSampleTest/blob/main/005/ui/control.go#L32-L37
ちゃんとディレクトリをわけてパッケージを分割するようにしてみた。分け方が正しいかはわからないが。
個々のControlがタッチを追跡する仕掛けなのでProcessTouch()でタッチの判定をして、自分に対するタッチ情報を保存してUpdate()で処理するようになっている。また、階層構造を構築するため、子になるControlは親の座標に対しての相対座標を持つ。描画やタッチ判定にはグローバルな座標が必要なので、それを算出するGetGlobalPos()もControlインターフェースに定義されている。

ControlBase構造体

共通する処理は共有したいのだが、Goには継承が無いので埋め込み構造体でどうにかするしかない。このとき選択肢が2つほどある。

  1. ControlBase構造体の中にControlインターフェースを埋め込み、ButtonやLabelといったControlを格納する。システムはControlBase構造体を扱う。
  2. ButtonやLabelといったControlの中にControlBase構造体を埋め込む。システムはControlインターフェースを扱う。

例えるなら、1.のほうは機械の体の中に人の精神が入ってるような状態で、世界とのやりとりは機械の体が行い、内面の自由は存在しているような感じ。公共のインフラなどは共通仕様の機械を相手にするだけでいいので効率が良い。機械の体に許されたアクションだけが個人と世界とのインターフェースになる。
2.のほうは生身の人間に世界とのインターフェースを取るための機械を埋め込む感じ。この機械があるからドアも開けれるし個人認証もできる。この機械がなくても機能をどうにか偽装できれば外部から侵入してもバレない。Goのインターフェースはメソッドさえ適切に存在していれば受け付けられるのである。
今回は2.の形で実装している。
https://github.com/mirichi/EbitenSampleTest/blob/main/005/control/button.go#L13-L19
ControlBaseさえ埋め込んでいればControlインターフェースを満たすことができて、タッチの処理やデフォルトの描画が実行できる。追加の処理が必要であればメソッドを実装する感じになる。例えばこう。
https://github.com/mirichi/EbitenSampleTest/blob/main/005/control/button.go#L61-L70

階層構造

階層構造を持つ必要性はメニューが原因になっているわけだが、Menu構造体の中には階層構造を扱うためのコードはほとんどない。実際にはControlBaseに大半を実装している。言い換えれば、ButtonやLabelでも配下のControlを持ちたければ持てる、という状態である。
https://github.com/mirichi/EbitenSampleTest/blob/main/005/ui/control.go#L39-L47
これを押し進めるとあらゆる機能が詰め込まれてControlBaseが巨大になってしまうので、例えば配下を持つための機能を分離して、ControlBaseを埋め込んだ別のControlContainerBaseみたいな構造体を作るべきかもしれない。しかしそうした場合いろいろなControlBaseができてくるわけだが、これらを組み合わせて何かを作るという機能はGoには無いのでちょっと困った感じになる。
まあ色んな人が試行錯誤してるだろうし参考に眺めてみたりするのもよさそうだ。

メニュー

ちょっと動かしてみようと思って強引にeasing機能を追加したのでコードがごちゃごちゃになってしまった。
メニューは単品ではなく、範囲外をタップしたときに終了できるようにするため、範囲外を現すメニュー画面(背景)のControlとセットになっている。
https://github.com/mirichi/EbitenSampleTest/blob/main/005/control/menu.go#L21-L28
https://github.com/mirichi/EbitenSampleTest/blob/main/005/control/menu.go#L92-L95
階層構造的にはメニュー画面(背景)が一番上で、その配下にメニューがいて、ラベルやボタンがいる形になる。タッチの判定は階層構造の下から行われるので、背景部分はメニューが無いところだけということになり、範囲外のタップを判定することができる。
この仕掛けはとりあえず作ったGUIシステムでこれを実現するために、わりかし強引に作った感じになる。もっとスマートにできればよかったのだが、こういうことをすることを想定していなかった。スマホとかどうにも使い慣れないオジサンなので。

おしまい

とりあえず動くようにはなっているが、このまま作りこんでいくと大変煩雑なコードに仕上がりそうな予感がビシバシする。言っても試作だし、こういう作り方をしたらどういう場合に困るのか、みたいな経験を積みながら何度か作り直せばマシなものになっていくのではなかろうか。そこまでやるかはわからんが。

Discussion