GodotのGDScriptふわっとした話
Godot Game Engineのスクリプト言語、GDScriptについて。Godotこれから始める人向け。
めちゃんこ概念的な話です。
具体的なコードの書き方は他にいくらでもいい記事があると思うので。
最終追加: 2023/10/02
役割
GDScriptはGodot Game Engineのスクリプト機能であり、Godot専用の言語です。役割はUnityのC#スクリプトとほとんど同じです。ゲームのロジックを記述します。
・ノード(UnityのGameObject)に振る舞いを追加する
・ノードの使うデータとその処理(つまりクラス)を定義する
役割はほぼ同じですが、アプローチが少し違います。
UnityではMonoBehaviour派生クラスを定義して、それをコンポーネントとしてGameObjectにアタッチします。Godotにおいても、GameObject的な概念が存在します。ノードと呼ばれるものです。ただし、スクリプトで記述するのは、ノード(Nodeクラス)そのものの派生クラスです。
この点は少しUnreal Engine的です。UEもGameObject的な存在であるActorの派生クラスを定義します。
言語
GDScriptはよくある高級言語と同じような機能を持っています。条件分岐、変数、定数、関数、クラス、列挙体、配列、連想配列、static変数、static関数、継承、コンストラクタ、デストラクタ、関数への参照、引数のデフォルト値、などです。
逆に、無い機能としては、構造体、抽象クラス、インターフェースクラス、多重継承、ref引数、out引数、LINQの類、などです。
Python同様、インデントが意味を持っていて、ブロックを形成します。波括弧{}の代わりにインデントする感じです。
# 例
func my_func(a: int, b int) -> bool:
if a > b:
return true
return false
func my_main() -> void:
var result: bool = my_func(3, 4)
print("result is " + str(result))
動的型言語ですが、静的に書くこともできます。違反した場合は、スクリプトのエディタがエラーを出してくれます。
var enemy: Enemy = Enemy.new(true, 100)
var weapon: Weapon = null
weapon = enemy # <- エラー
動的に書いた場合、型安全でない行は、コードエディタ上で行番号の色が変わって通知されます。
# 型安全でない行は、行番号がグレーアウトします
var enemy: Enemy = null
func my_func(arg1):
if arg1.has_weapon() == true: # ←arg1がhas_weapon関数を持ってるとは限らない
return false
enemy = arg1 # ←arg1がEnemyクラスとは限らない
return true
func my_main():
var result: bool = my_func(first_enemy) # ←my_funcがboolを返すとは限らない
C#
ご存知の通り、GodotではC#も使えます。C#を使う場合、C#機能の付いた別のGodotエディタをダウンロードします。Visual Studioなどを使って記述するようです。正直よく知りません。ごめんなさい。
新しくGodot始める人がC#使いたい気持ちは分かるのですが、GDScript、言語自体は悪いものではないです。ほんとほんと。
GDScriptだけを使う利点は、制作環境も実行環境も簡素であること。あと、移植性を少し保てることです。移植先(ゲーム機とか)でMonoや.NETが使えるとは限らないので。
メモリ管理
GDScriptはガベージコレクション式のメモリ管理ではありません。クラスのインスタンスは参照カウンタで管理されます。公式ドキュメントによると、ガベコレはゲームと相性が良くないからという判断のようです。
厳密には、GDScriptのRefCountedというクラスの派生クラスが、自動でメモリ返却される対象です。GDScriptにおけるすべてのクラスの親はObjectクラスですが、RefCountedではないObjectは責任もって手動で破棄(free関数)する必要があります。
ノード(Node)はRefCountedではないですが、ノードはその役割的に、意識的に破棄(free)するようコードを書くはずです。ノードは親子付けできますが、親をfreeすると子ノードや孫ノードも自動的にfreeされます。
結果的に、自作ノードはNode(Nodeやその派生)を継承して作って、非ノードの自作クラスはたいていの場合RefCountedをベースにして作ることになります。こうしている限りは、そんなにfree忘れを意識しなくて大丈夫です。
派生関係
Object
<- ユーザが触らないたくさんのクラス
<- Node
<- Node3D
<- たくさんの3D系ノードクラス
<- CanvasItem
<- たくさんの2D系ノードクラス
<- RefCounted
<- Resource
<- たくさんのリソースクラス
(参照カウンタによるメモリ管理の常として、循環参照による孤立は気を付ける必要はあります。)
ファイル
拡張子は.gdです。コメントに日本語使えます。
GDScriptは1クラス1ファイルです。Unityも建前としてMonoBehaviour派生クラスは1クラス1ファイルでしたが、Godotの場合はもっと厳格です。
そんなわけでenumは必ずクラスに属しています。クラスに属さないグローバル変数やグローバル関数はありません。クラス内クラスは使用できます。
エントリーポイント
基本的な考えはUnityと同じです。通常のC++/C#実行環境のような、main関数に相当するようなエントリーポイントはありません。
ゲーム起動時にロードされる初期シーン(Unityのシーンと同じです)に存在するノードの、初期化関数_init()/_ready()とその更新関数_process()が、すべての起点になります。
Node
Nodeがゲームを駆動させる基本機能になります。
UnityのGameObject同様、Node同士でツリー状の親子関係を構築し、それぞれが2Dまたは3D空間上の相対座標を持っています。
そしてUnityのMonoBehaviour同様、初期化関数_init/_readyと更新関数_processを持っています。
例えば3Dモデルを表示するには、MeshInstance3Dノードを使います。これは、UnityのGameObject+MeshRendererに相当するものです。
武器を表現するWeaponクラスが必要になった場合はどうしましょう。そんな時は、MeshInstance3Dクラスを派生してWeaponクラスを定義するといいかもしれません。(なんか英語的な文章だな)
武器のモデル表示をベースクラスで実現しつつ、新たに耐久値を保持させたり、当たり判定ノードを子ノードとして持たせたりできます。
もしくは、もっと純粋な空ノード(Node3D)をベースにWeaponクラスを作って、その子ノードとしてMeshInstance3Dノードを持たせてもいいかもしれません。これなら不要な時は子ノードを消してモデルそのものを非表示にしたり、逆に子ノードを複数持って複数のモデルからなる武器を表現できます。
スクリプトのアタッチ
Node派生クラスのインスタンス(つまりノード)を作成するにはnew関数を呼びます。C系の言語のnewと同じです。
var my_node: MyNode = MyNode.new(100, true, "Apple")
また、エディタのGUIから、編集中のシーンにノードを追加するときに、一覧に現れるようになるので、そこから追加することもできます。
でもね、作ったノードを使うには別の方法もあるんです。
エディタのGUI上で、既存ノードにスクリプトをドラッグ&ドロップします。するとノードにスクリプトがアタッチされます。アタッチはゲーム実行中にスクリプトから実施することも可能です。
ここら辺はUnityのスクリプトのアタッチ機能と同じですね。
ちょっと待って!
Q. NodeはUnityのGameObjectみたいなものじゃないの?
A. そうです。
Q. GameObjectとコンポーネントが一体化したようなものなんでしょ?
A. そうです。
Q. じゃあノードにNodeをアタッチするって、どういうことだよ!?
A. そう思うよね。
組み込みノードにNode派生クラスのスクリプトをアタッチすると、機能が共存するような感じになります。多重継承的な感じです。
例えばMeshInstance3Dノードは、3Dの見た目を提供するノードですが、これに自作の「スペースキーを押したらジャンプする」Nodeスクリプトをアタッチすれば、スペースキーを押したらジャンプする3D見た目を持つノードになります。
いくつかルールがあります。
アタッチするスクリプトは自作Nodeクラスのみ。
組み込みNodeクラスはアタッチできない。
(やり方はあるかもしれないけど普通ではない)アタッチされる側が、組み込みノードの場合と、自作ノードの場合で動作が異なる。
組み込みノードへのアタッチは機能が「共存」される。
自作ノードへのアタッチは、機能が「上書き」される。同名の関数はアタッチした側でオーバーライドされる。
1ノードにアタッチできるのは1スクリプトのみ。
ゲーム作るにあたって、アタッチ機能は必須ではないです。ただ、楽になったり、実装がスマートになったりすることはあります。
スコープ
GDScriptにはスコープの概念がありません。
すべてのクラスは宣言なく使え、そのメンバ変数・メンバ関数もすべてpublicです。ただし慣習として、privateとして扱うメンバはアンダースコアを名前の頭につけることになっています。
また、特定の宣言(@export)を付けたメンバ変数は、エディタのGUIから編集可能です。これは、Unityの[Serializable]宣言と同じですね。
ところで、実はスクリプトにはクラス名を付けない選択ができます。
# クラス名のあるNode3D派生クラス
class_name MyNode3D
extend Node3D
以下クラス定義
# クラス名の無いNode3D派生クラス
extend Node3D
以下クラス定義
クラス名を付けない場合、結果的にスコープ外のような扱いになります。どこからも呼べません。そもそも名前無いしね。
この無名クラスのスクリプトを使う場合、次の2つの使い方があります。
・事前にエディタのGUI上でノードにアタッチする。
・実行時にスクリプトをロードして、ノードにアタッチする。
スクリプトはリソース(アセット)の一種であり、ロード可能です。
急にGodotの記事のアクセスが増えたので、取り急ぎ書いてみました。
そのうち追記するかも?