【Godot4.x】gdscriptを静的型付け言語として使う方法

今回は、gdscript を静的型付け言語(っぽく)使う方法について紹介します。 gdscriptは本来は型を決めずにプログラムを書ける動的型付け言語ですが、書き方や設定方法によってはC#のように型に厳しい書き方ができます。

1. 明示的な型アノテーションを使う

例えば以下の変数 "hp" は明確な型が指定されていないため、自由な値を代入できてしまいます。

# 型アノテーションなし.
var hp = 100

func _ready() -> void:
    hp = 100.123
    print(hp) # 100,123と出力される.
    
    hp = "hero" # 文字列も代入可能.
    print(hp) # "hero" と出力される

型指定がないと、float値でも文字列でも代入できます。

そこで「:」の後に明示的に型を指定します。 以下の例では "int" を指定しているので、float型を代入しても int型に丸められます。

# intを明示的に指定.
var hp:int = 100

func _ready() -> void:
    hp = 100.23
    print(hp) # 100と出力される

さらに文字列を代入しようとすると構文エラーとなります。

なお@exportを使うときに型指定を行うとエディタ上で設定する際に対象を制限できるようになります。

@export var BULLET_OBJ:PackedScene

型推論付き代入

アノテーションを使う変わりに「:=」で代入すると「型推論」により型が決まります。

# intを明示的に指定.
var hp := 100

func _ready() -> void:
    hp = "hero" # 文字列を代入すると構文エラー.

「型アノテーション」と「型推論」は型を決める機能としてはどちらでも同じなので、明示的に型を記述したい場合は「型アノテーション」、型名が長くて書きたくない場合などは「型推論代入」を使う、などといった使い分けが良いかもしれません。

型の指定がない場合はエラーにする方法

型の指定を強制したい場合はプロジェクト設定 > デバッグ > GDScript から "Untyped Declaration" を「Error」にすると、型の指定のない変数宣言をエラーにすることができます。

ArrayとDictionaryの型指定

ArrayとDictionaryはそれぞれ [] と {} で初期化できますが、何も型を指定しないと何でも入る自由な変数となってしまいます。

var arr = []

func _ready() -> void:
    arr.append(100) # 数値が入ります.
    arr.append("hero") # 文字列も入れられます.

Arrayの型を指定したい場合は Array[xxx] というアノテーションを指定します。

var arr:Array[int] = []

func _ready() -> void:
    arr.append(100) # 数値が入ります.
    arr.append("hero") # 文字列は入りません.
    print(arr) # [100]と出力されます.

代入は禁止されますが、実行はできてしまいます。 ただデバッガのログとしてはエラー出力されます。

Dictionaryは Dictionary[xxx, xxx]というアノテーションを指定します。

var dict:Dictionary[String, int] = {}

func _ready() -> void:
    dict["hp"] = 100
    dict["mp"] = "20" # 構文エラーとなる.

Dictionaryについては、間違った代入などがあると、エディタ上でエラー表示されます。

注意点として、入れ子構造にすると型指定はできません。 (Godot 4.4現在)

var teams: Array[Array[Character]] = [] # この定義はできない.
var dict_list:Dictionary[String, Array[int]] = {} # この定義もできない.

そのため入れ子構造にしたい場合は、独自定義の型 (classを使う) にする必要があります。

関数の引数、戻り値の型指定

関数は引数、戻り値ともにアノテーションによる型の指定が可能です。

func _add(a:int, b:int) -> int:
    return a + b

なお「エディタ設定」から「テキストエディター > 自動補完」の "型ヒントを追加" を ONにすると、関数を補完入力したときに自動で型アノテーションがついて便利です。

クラス名を定義する

シーンを preload() (または@exportPackedSceneを読み込む ) して instantiate() で生成する場合、クラス名を定義しておくと型が明示的になります。 例えば敵弾クラスで class_nameキーワードを使って Bulletクラスを宣言します。

extends Node2D

# クラス名を定義.
class_name Bullet

するとこのシーンからインスタンスを作るときに明示的に型を指定できます。

extends Node2D

const BULLET_OBJ = preload("res://Enemy.tscn")

func _bullet() -> void:
    # 型を明示的に宣言できます.
    var b:Bullet = BULLET_OBJ.instantiate()

クラスの型を明示的にするメリットとしては、コード補完が有効になるほか、is キーワードで型を比較して条件分岐が可能となります。

   var b:Bullet = BULLET_OBJ.instantiate()
    if b is Bullet:
        print("bはBulletクラスです")

定数 (const)・列挙型 (enum) を使う

マジックナンバーの変わりに定数 const を使うことで、コードの意味が明確になります。

player = PLAYER_OBJ.instantiate()
player.position = Vector2(400, 400) # 出現位置を指定.

const START_POS := Vector2(400, 400) # 出現位置.

....

player = PLAYER_OBJ.instantiate()
player.position = START_POS

また状態変数など、1つの変数が決まった状態で動く場合には enum で定義できます。

# プレイヤーの状態.
enum eState {
    IDLE,
    WALK,
    JUMP,
}

var _state := eState.IDLE

型キャスト (変換)

asキーワードで明示的に型を変換できます。

   var speed:float = 300.567
    var x := speed as int
    print(x) # 300

なお型変換に失敗した場合はnullが入る、またはエラー扱いとなります。

# 衝突シグナル
func _on_area_entered(area: Area2D) -> void:
    var other := area as Player
    if other != null:
        other.vanish() # Playerなら消滅.
const ENEMY_OBJ = preload("res://Enemy.tscn")

func _ready() -> void:
    var enemy:Enemy = ENEMY_OBJ.instantiate()
    var tmp := enemy as Bullet # 継承関係のない型には変換できない (エラー)

なおasはセーフライン (無効なら実行されない行) として使うことも可能です。

# 衝突シグナル
func _on_area_entered(area: Area2D) -> void:
    (area as Player).vanish() # Playerなら消滅させる (それ以外は実行されない).

参考

docs.godotengine.org