GDScript: 動的言語の紹介

概要

このチュートリアルでは、GDScriptをより効率的に使用する方法を簡単に紹介します。この言語固有の一般的なケースに焦点を当てていますが、動的型付け言語に関する多くの情報も扱っています。

これは、動的型付け言語に関する経験がほとんどないかまったくないプログラマに特に有用であることを意味します。

動的性質

動的型付けの長所と短所

GDScriptは動的型付け言語です。その主な利点は以下のとおりです:

  • 言語はシンプルで習得が容易です。
  • ほとんどのコードは、手間をかけずに素早く作成および変更できます。
  • 書かれたコードが少なければ少ないほど、エラーや修正の誤りが少なくなります。
  • コードがより読みやすいです(混乱が少なくなります)。
  • 動作テスト時にコンパイルは必要ありません。
  • ランタイムは小さいです。
  • ダック・タイピングとその性質による多型。

主な欠点は次のとおりです:

  • 静的型付き言語よりもパフォーマンスが劣ります。
  • リファクタリングするのがより難しい(シンボルは辿ることができない)
  • 静的型付け言語のコンパイル時に通常検出されるエラーの中には、コードの実行中にのみ発生するものがあります(式の構文解析がより厳密なため)。
  • コード補完の柔軟性が低い(一部の変数型は実行時にのみ認識されます)。

これは、実際には、Godot + GDScriptはゲームを迅速かつ効率的に作成するために設計された組み合わせです。非常に計算集約的で、エンジンの組み込みツール(ベクトル型、Physics Engine、Mathライブラリなど)の恩恵を受けることができないゲームには、C++を使用する場合もあります。GDScriptでゲームの大部分を作成し、パフォーマンスの向上が必要な部分にC++の一部を追加することができます。

変数及び割り当て

動的に型付けされた言語の変数はすべて「Variant型」のようなものです。つまり、タイプは固定されず、割り当てによってのみ変更されます。例:

静的:

int a; // Value uninitialized.
a = 5; // This is valid.
a = "Hi!"; // This is invalid.

動的:

var a # 'null' by default.
a = 5 # Valid, 'a' becomes an integer.
a = "Hi!" # Valid, 'a' changed to a string.

関数の引数として:

関数も動的な性質を持っているため、次のように異なる引数で呼び出すことができます。例えば:

静的:

void print_value(int value) {

    printf("value is %i\n", value);
}

[..]

print_value(55); // Valid.
print_value("Hello"); // Invalid.

動的:

func print_value(value):
    print(value)

[..]

print_value(55) # Valid.
print_value("Hello") # Valid.

ポインタと参照:

CやC++(そしてある程度はJavaとC#)などの静的言語では、変数と、変数へのポインタまたは参照とは区別されます。後者では、元のオブジェクトへの参照を渡すことで、他の関数によってオブジェクトを変更できます。

C#やJavaでは、組み込み型(int、float、場合によってはString)でないものはすべて常にポインタまたは参照です。参照も自動的にガベージコレクションされるため、使用されなくなると削除されます。動的型付け言語もこのメモリー・モデルを使用する傾向があります。例:

  • C++:
void use_class(SomeClass *instance) {

    instance->use();
}

void do_something() {

    SomeClass *instance = new SomeClass; // Created as pointer.
    use_class(instance); // Passed as pointer.
    delete instance; // Otherwise it will leak memory.
}
  • Java:
@Override
public final void use_class(SomeClass instance) {

    instance.use();
}

public final void do_something() {

    SomeClass instance = new SomeClass(); // Created as reference.
    use_class(instance); // Passed as reference.
    // Garbage collector will get rid of it when not in
    // use and freeze your game randomly for a second.
}
  • GDScript:
func use_class(instance): # Does not care about class type
    instance.use() # Will work with any class that has a ".use()" method.

func do_something():
    var instance = SomeClass.new() # Created as reference.
    use_class(instance) # Passed as reference.
    # Will be unreferenced and deleted.

GDScriptでは、基本型(int、float、string、およびベクトル型)のみが値によって関数に渡されます(値はコピーされます)。それ以外のもの(インスタンス、配列、辞書など)は参照として渡されます。 Reference を継承していないクラス(何も指定されていない場合のデフォルト)は、使用されていないときは解放されますが、 Object から手動で継承している場合は手動のメモリ管理も可能です。

配列

動的型付け言語の配列は、内部に多くの異なるデータ型を混在させることができ、常に動的です(いつでもサイズ変更可能)。たとえば、静的型付け言語の配列を比較します:

int *array = new int[4]; // Create array.
array[0] = 10; // Initialize manually.
array[1] = 20; // Can't mix types.
array[2] = 40;
array[3] = 60;
// Can't resize.
use_array(array); // Passed as pointer.
delete[] array; // Must be freed.

// or

std::vector<int> array;
array.resize(4);
array[0] = 10; // Initialize manually.
array[1] = 20; // Can't mix types.
array[2] = 40;
array[3] = 60;
array.resize(3); // Can be resized.
use_array(array); // Passed reference or value.
// Freed when stack ends.

GDScriptでは:

var array = [10, "hello", 40, 60] # Simple, and can mix types.
array.resize(3) # Can be resized.
use_array(array) # Passed as reference.
# Freed when no longer in use.

動的型付け言語では、配列はリストなどの他のデータ型のように二重になることもあります:

var array = []
array.append(4)
array.append(5)
array.pop_front()

または順序なしセット:

var a = 20
if a in [10, 20, 30]:
    print("We have a winner!")

辞書

辞書は動的型付け言語における強力なツールです。静的型付け言語(C++やC#など)出身のプログラマーの多くは、その存在を無視し、不必要に彼らの生活を困難にしています。このデータ型は通常、そのような言語には存在しないか限定された形でのみ存在しています。

辞書は、キーまたは値として使用されるデータ型を完全に無視して、任意の値を他の任意の値にマップできます。一般に信じられていることとは異なり、ハッシュテーブルを使用して実装できるため効率的です。実際、これらは非常に効率的なため、一部の言語では辞書として配列を実装することもあります。

辞書の例:

var d = {"name": "John", "age": 22} # Simple syntax.
print("Name: ", d["name"], " Age: ", d["age"])

辞書も動的で、キーの追加や削除はいつでも低コストで行えます:

d["mother"] = "Rebecca" # Addition.
d["age"] = 11 # Modification.
d.erase("name") # Removal.

ほとんどの場合、辞書を使用すると2次元配列をより簡単に実装できます。以下は簡単な戦艦ゲームの例です:

# Battleship Game

const SHIP = 0
const SHIP_HIT = 1
const WATER_HIT = 2

var board = {}

func initialize():
    board[Vector2(1, 1)] = SHIP
    board[Vector2(1, 2)] = SHIP
    board[Vector2(1, 3)] = SHIP

func missile(pos):
    if pos in board: # Something at that position.
        if board[pos] == SHIP: # There was a ship! hit it.
            board[pos] = SHIP_HIT
        else:
            print("Already hit here!") # Hey dude you already hit here.
    else: # Nothing, mark as water.
        board[pos] = WATER_HIT

func game():
    initialize()
    missile(Vector2(1, 1))
    missile(Vector2(5, 8))
    missile(Vector2(2, 3))

辞書は、データマークアップまたはクイック構造としても使用できます。 GDScriptの辞書はPythonの辞書に似ていますが、Luaスタイルの構文と索引付けもサポートしているので、初期状態や素早い構造体を書くのに役立ちます:

# Same example, lua-style support.
# This syntax is a lot more readable and usable.
# Like any GDScript identifier, keys written in this form cannot start
# with a digit.

var d = {
    name = "John",
    age = 22
}

print("Name: ", d.name, " Age: ", d.age) # Used "." based indexing.

# Indexing

d["mother"] = "Rebecca"
d.mother = "Caroline" # This would work too to create a new key.

Forとwhile

静的型付け言語の中には、反復処理が非常に複雑なものがあります:

const char* strings = new const char*[50];

[..]

for (int i = 0; i < 50; i++) {

    printf("Value: %s\n", i, strings[i]);
}

// Even in STL:

for (std::list<std::string>::const_iterator it = strings.begin(); it != strings.end(); it++) {

    std::cout << *it << std::endl;
}

これは通常、動的型付け言語では非常に単純化されています:

for s in strings:
    print(s)

コンテナのデータ型(配列と辞書)は反復可能です。 辞書ではキーを繰り返すことができます:

for key in dict:
    print(key, " -> ", dict[key])

インデックスによる反復も可能です:

for i in range(strings.size()):
    print(strings[i])

range()関数は3つの引数を取ることができます:

range(n) # Will go from 0 to n-1.
range(b, n) # Will go from b to n-1.
range(b, n, s) # Will go from b to n-1, in steps of s.

静的型付けされたプログラミング言語の例:

for (int i = 0; i < 10; i++) {}

for (int i = 5; i < 10; i++) {}

for (int i = 5; i < 10; i += 2) {}

動的型付け言語では:

for i in range(10):
    pass

for i in range(5, 10):
    pass

for i in range(5, 10, 2):
    pass

逆方向ループは負のカウンタで行われます:

for (int i = 10; i > 0; i--) {}

動的型付け言語では:

for i in range(10, 0, -1):
    pass

While

while()ループはどこでも同じです:

var i = 0

while i < strings.size():
    print(strings[i])
    i += 1

カスタム・イテレータ

デフォルトのイテレータがニーズに合わない場合は、スクリプト内のVariantクラスの _iter_init_iter_next 、および _iter_get 関数をオーバーライドして、カスタムイテレータを作成できます。転送イテレータの実装例を次に示します:

class ForwardIterator:
    var start
    var current
    var end
    var increment

    func _init(start, stop, increment):
        self.start = start
        self.current = start
        self.end = stop
        self.increment = increment

    func should_continue():
        return (current < end)

    func _iter_init(arg):
        current = start
        return should_continue()

    func _iter_next(arg):
        current += increment
        return should_continue()

    func _iter_get(arg):
        return current

これらは他のイテレータと同様に使用できます:

var itr = ForwardIterator.new(0, 6, 2)
for i in itr:
    print(i) # Will print 0, 2, and 4.

_iter_init のイテレータの状態を必ずリセットしてください。リセットしないと、カスタムイテレータを使用するネストされたforループが期待どおりに動作しません。

ダック・タイピング

静的型付け言語から動的型付け言語に移る場合において、とりわけ理解するのが難しい概念の1つが、ダック・タイピングです。ダック・タイピングを使用すると、コード全体の設計は非常に単純かつ明確になります。しかし、その仕組みは明白ではありません。

例として、大きな岩がトンネルを流れ落ちて、途中ですべてを壊している状況を想像してください。静的に型付けされた言語でのロックのコードは、次のようになります:

void BigRollingRock::on_object_hit(Smashable *entity) {

    entity->smash();
}

このように、岩石によって破壊されることができるすべてはSmashableを継承しなければならないでしょう。キャラクター、敵、家具、小さな岩がすべて粉砕可能であれば、それらはSmashableクラスから継承する必要があり、おそらく複数の継承が必要になります。 多重継承が望ましくない場合は、Entityのような共通クラスを継承する必要があります。それでも、仮想メソッド smash() をEntityに追加するのは、それらのうちのいくつかを粉砕できる場合に限り、それほどエレガントではありません。

動的型付け言語ではこれは問題になりません。ダック・タイピングでは、必要に応じて smash() 関数を定義するだけで済みます。継承、基本クラスなどを考慮する必要はありません。

func _on_object_hit(object):
    object.smash()

以上です。 大きな岩に当たったオブジェクトがsmash()メソッドを持っていれば、それが呼ばれます。継承や多型は必要ありません。動的に型付けされた言語は、それが継承しているものやクラスの型ではなく、目的のメソッドやメンバを持つインスタンスだけを扱います。ダック・タイピングの定義はこれをより明確にするはずです:

「アヒルのように歩きアヒルのように泳ぎアヒルのように鳴く鳥を見たら、私はその鳥をアヒルと呼びます」

この場合は、次のように変換されます:

「オブジェクトが壊れる可能性がある場合は、それが何であるかは気にしないで、単にそれを壊してください。」

はい、代わりにHulkタイピングと呼ぶべきですね。

ヒットしたオブジェクトがsmash()関数を持たない可能性があります。動的に型付けされた言語の中には(Objective Cのように)存在しないメソッドの呼び出しを単に無視するものがありますが、GDScriptはより厳密なので、関数が存在するかどうかをチェックするのが望ましいです:

func _on_object_hit(object):
    if object.has_method("smash"):
        object.smash()

そして、その方法を単純に定義すれば、岩に触れるものは何でも砕くことができます。