LoginSignup
9
7

静的型付け大好きプログラマによるGodotEngine(C#)への理解

Last updated at Posted at 2023-12-07

バージョンとか

Godot4.1.3 + C#

この記事の意義

公式ドキュメントが見づらい。いや、仕方がないのだ。
まだまだ発展途上だし極東の小国のためだけに充実した日本語でわかりやすーくドキュメントを整備する暇などないだろう。

じゃあ、とGoogle検索をかけてみるとおなじみのサイトが立ち並ぶ。
GDScript多いな!
> ほにゃほにゃをほいだらしたらこんなものができます!
違うの、知りたいのはそういうことじゃないの。
自分の作りたいものとはまるで趣向の違うゲームを作り始めるチュートリアルではなくて、何がどうやって動いているのか。
プログラム的にはこの操作にどういった意義があってバグが起きたときはどこに注目したらいいのか。
どうやったら成功するかだけでなくどうやったら失敗するのか。

読みやすい日本語でプログラマ視点の解説が読みたい!

……自分で書くか!

注意

Godot歴が浅いうえに公式ドキュメントを斜め読みしているので間違った記述があるかもしれません。
おそらく偉い方々がコメント欄で教えてくれると思うので必ず目を通すように。

また、プログラミングの基本的なところについては解説しないので、各自で競プロ茶色くらいになってくるように。
あと、もしかしたらここらへんちゃんと書いてくれてるページがあるかも! 二番煎じだったらゴメン!

GodotでC#を使うことについて

C#のメリット

GDScriptの利点はいろいろあるが、根底が動的型付けなので編集しずらい。
私はC#が高速~とかはどうでもよく、静的型付けによる編集上での利点を重視して使っている。
詳しくはこちら→https://sites.google.com/view/hyahoi/godot%E8%A6%9A%E6%9B%B8/gdscript%E3%81%95%E3%82%8F%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%9F

またC#を使う上での懸念として未対応機能があるのではないか……とのことだが、WebGLビルドが現時点で制限されていることを除けばおおまかには問題なさそうだった。
とはいえこのあたりバージョン・用途によるものも大きいと思うので各自で調べてくれ。

セットアップでの罠

https://godotengine.org/download/windows/
GodotEngineには.NETサポートがあるほうとないほうがある。
Steamで取ってこれるのはないほうで、こっちだとC#が使えないことに注意だ。
ついでに起動時に.NETをインストールしろと言われる。
執筆時点での最新バージョンは7だったのでこれを入れる。
プロジェクトが立ち上がって適当なシーンを作り、この巻物プラスみたいなボタンを押して言語からC#が選べるようになってたらOKだ。
image.png
外部エディタは下記で推奨されている通りどれかを選んで設定すべし。
https://docs.godotengine.org/ja/4.x/tutorials/scripting/c_sharp/c_sharp_basics.html#configuring-an-external-editor

GDScriptのあの機能はどうやって使う?

Signalなどの特殊なものは公式ドキュメントに載っているのでここは全部目を通すしかない。
それ以外でのグローバルスコープのものは大体GD.って書けば出てくる。
ちょくちょく関数名が変わっていたりするが、静的型付けのおかげで候補が絞られているので全部目を通せばたぶん対応してそうなのが見つかるはず。

Godotではどこをスクリプトで書くことになるのか?

Godotエンジンが
「どうせお前ゲームでこういう要素いれたがるだろ?」
というものはNodeとして用意されている。
それらの組み合わせでもどうにもならないようなカスタマイズされた動きをさせたいときに初めてスクリプトを書くことになる。
このスクリプトがアタッチされることで本当になんでもできるようになるのだが、基本的な使い方としてはノードにくっつけてなにかのきっかけをもとにそのノードの状態・性質をいじる、というものが多い。
ついでにだがGUIと連携するための機能も用意されているのでエディタ拡張とかExportとかについて調べてみてもいいかもね。

アタッチされたスクリプトは何をしているのか?

デフォルトスクリプトを見てみよう

using Godot;
using System;

public partial class World : Node2D
{
	// Called when the node enters the scene tree for the first time.
	public override void _Ready()
	{
	}

	// Called every frame. 'delta' is the elapsed time since the previous frame.
	public override void _Process(double delta)
	{
	}
}

Unityを触っている人なら話は早いのだが、
_readyがStart、_processがUpdateみたいな役割を行う。

ゲームプログラミングでは動的に動画を作るようなものなので毎フレーム行う処理がありこれが_process
そして最初だけ特別に処理したい場合が多いのでこれを_ready
の中で行うというわけだ。
ちなみにC#の関数としてはアンダーバーなしパスカルケースで書かなければならず命名規則から外れていることになるが、この名前でないとシステムから呼び出されないのでしぶしぶこれに従うことにしよう。

ちなみにこのときの呼び出し元はソースジェネレータでできたソースコードだ。編集できはしないが覗いてみてもいいかもね。(クラスがpartialなのはこの自動生成スクリプトとくっつけるためと思われる)

ここで作ったクラス、WorldはNode2Dを継承している。
このスクリプトがアタッチされているノードは、Node2Dを継承したWorldとしての性質を持つことになる。
Unityの考え方と違い、スクリプトをアタッチするというのはノードをアタッチしたスクリプトで記述したようなノードに変容させる、という意味であると解釈できる。

ただしここでのWorldはほぼNode2Dのまま、そしてこれはGodot世界におけるただそこに存在するというだけの何かなので基本的な移動以上のなにかができるわけでもない。
ただしArea2Dから継承してスクリプトを書けばエリアへの侵入を検知したりするのでいよいよゲームっぽくなる。
じゃあそれをどうやるのか。

Signal

Signalについて知ろう

Godotはなにかが起きたことを伝えるとき「シグナル」を使う。
たとえばArea2Dはエリア内に当たり判定を有する剛体が入ってきたら「body_entered」シグナルを発する(emitする)。
特定のノードが発するシグナルはノードを選択した状態で画面右側のノード>シグナルから確認できるからヨロシクな!
image.png
このシグナルはスクリプトから独自定義したものを発することもできる。
そしてシグナルに紐付けられた(connectされた)関数がすべて呼び出される。

そういう概念のものなのだが……

C#でのSignalの扱いについて知ろう

C#に慣れている人ならシグナルの使い方はなんだか聞き覚えのある……そう、イベント(EventHandler)だなこれ。
ということで、Godot上でのシグナルはC#上でイベントとしてアクセスできる。
ノードメンバの好きなイベントに好きなように関数を+=したり-=してくれたまえ。

また逆にC#のイベントをSignalとして扱えるようにできる。
イベントを定義するdelegateの宣言に[Signal]を付け加えて名前末尾をEventHandlerとするだけである。
詳しくはこちら→https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/c_sharp_signals.html#custom-signals-as-c-events

Signalを利用するのはなぜ?

スクリプト上で直接参照を持ち呼び出せばよいではないか
と思うのはごもっともだがこういったつくりにすることで疎結合が実現しやすい。
シグナルを発する側はこれにより何が起きるかを感知しないのが重要で、これは自然にTell, Don't Askを強制できる。

ただしEventHandler=シグナルでないのは、細かい仕様が異なるからである。
シグナル側が動的型付けなだけでなく、ConnectしたCallable関数の持ち主インスタンスが消えたとき自動的にDisconnectされるなど……

GDScriptとC#でSignalの扱いがどれくらい違うか

extends Node2D

func _ready():
	pass

func _process(delta):
	pass

func _on_area_2d_body_entered(body):
	if body.name == "Player":
		body.queue_free()

GUI上で紐付けることができて関数の横にアイコンが出る。
けどスクリプト上での一覧性はよくない。
あと型がわからんので実行時エラーに苦しめられがち。
connectをし実行時にシグナルの接続を行う方法もあるがやはり型に苦しめられる。

using Godot;

public partial class Mokeke : Node2D
{
	[Export] Area2D Area2D { get; set; }

	public override void _Ready()
	{
		Area2D.BodyEntered += AttackPlayer;
	}

	void AttackPlayer(Node2D body)
	{
		if (body.Name == "Player")
		{
			body.QueueFree();
		}
	}

	public override void _Process(double delta)
	{
	}
}

書く行数は増えるが圧倒的高凝集!
デリゲートの型を間違えてると当然コンパイルエラーが出てくれる。

コード外部との連携

アタッチ先以外の他のノードと連携する

おおまかに上記を参照してもらうとして……
ここでstringを使って種類を決めてノードを取得することで、そのノードについて編集を行うことができるようになる。
またexportを使ってGUI上で予め参照するノードを決めることも可能だ。

アタッチ先以外の他のスクリプトと連携する

さらにさらに、その取得したノードをダウンキャストすることでそのノードにアタッチしているスクリプトを取得できる。
繰り返しになるがノードにスクリプトをアタッチするというのはそのノードがスクリプトを所持するのではなくスクリプトがそのノードを継承した新しい子ノードに作り変えるイメージである。
なので自分で宣言したクラスと同種のノードがあるものとして取得を行える。
exportなどで取得すれば動的なダウンキャストを避けてアクセスすることができる。

以下はGDScriptでないと受けられない恩恵

image.png
型の変更でのノード種類一覧に表示されるようになる
(C#だと表示されないので手動アタッチが必要)

image.png
exportでのインスタンス選択でもハイライトされる
(C#だと特定できないのですべてがハイライトされる)

GDScriptで書かれたクラスと連携する

この型は問答無用でGDScriptになる。たとえclass_nameを設定していようとC#からは直接参照できない。
というのも、GDScriptは根源的に動的型付けであり静的型付けとして扱うには無理がある。
(全delegateに対応するものがCallableというただ一種の型扱いで、signalの紐づけの引数にstringを要求してくるような言語なので)
C#の流儀に則ればdynamic扱いかもだが、多少の制約くらいはつけられるのでGDScriptとして扱う。
詳しくは下記参照。
https://docs.godotengine.org/en/stable/tutorials/scripting/cross_language_scripting.html

ノードの破棄と参照の破棄について

やりかた自体は上記の通り。
C#はC++と違いメモリ解放はGC(ガーベッジコレクション)を使う。
使わなくなったオブジェクトを明示的に特定タイミングでメモリ解放し、デストラクタでついでに諸々を消去するといった手法は一般的ではないのだ。
ではどうするかというとデストラクタよりも前のタイミングで消去用の処理だけ明示的に呼び出す。メモリ云々についてはいつ呼び出されてもよいものとして分離される。

ここでの明示的な呼び出しがNode.QueueFreeというわけだ。
さて、これはC#のシステム的な消去ではないのは上記に書いた通り。
ということは消去されたかどうかについて調べるのもまたC#のシステム機能では完結しない。

if(IsInstanceValid(Target))
{
    // Targetに対して行う処理
}

こうしてやることでTargetがGodot的に有効なNodeであるかを確認してくれる。
nullな場合は当然有効でないし、nullでなかったとしても上記関数ですでに消去されているノードであったら有効でない。
なのでこの関数を使うべし、というわけだな。

(上記Targetについてメンバ変数などで参照を持ち続けていた場合はそもそもGCの解放の対象にならない。参照nullは有効なNodeかどうかの判断にあまり寄与しないことに気をつけよう)
(DisposeはIDisposableインターフェースが実装するメソッドなはずで、おそらく内部的な処理に使うんじゃないかなあ、しらんけど)

その他

C#のexportは.NETビルドが通るまで反映されない。
再生ボタン……でもよいのだが横にひっそり存在するBuildボタンを押そう。
image.png

Nodeを継承したスクリプトからも継承が可能。
すでにアタッチされたノードを右クリックしてスクリプトを拡張する。
image.png

日本語を書くと怒られる?
これを読め!(自分の記事宣伝)

インスタンス化はインスタンス化可能なリソースに対して行うことになる。
これはGD.Loadの返り値Resourceを継承したPackedSceneにダウンキャストできるかで検査できる。
PackedSceneはInstantiateメソッドを持ち、これによりインスタンス化したのちそのシーンのrootとなるNodeが返される。それのシグナルやアタッチされたスクリプトにアクセスしたい場合にはまたダウンキャストを行うことになる。
静的型付けっぽくないアプローチだが、このあたりはUnityも似たようなものなのである程度仕方ないと割り切っていこう。

まとめ

GDScriptと直接連携しようとするといろんな制約がかかる。
それ以外についてはソースジェネレータと静的型付けされているライブラリのおかげで思ったより快適に操作できる印象。
GDScriptに比べるとGUIとの連携が甘いところもあるが軽く触った感じは全く不自由なく静的型付けできることのメリットが大幅に上回る。

いかがでしたか?

9
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7