見出し画像

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のスクリプトのアタッチ機能と同じですね。

round_shield.gsをアタッチ。ダメージ値が編集可能に

ちょっと待って!

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の記事のアクセスが増えたので、取り急ぎ書いてみました。
そのうち追記するかも?

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