Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

GDScript: 動的言語の紹介

概要

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

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

動的性質

動的型付けの長所と短所

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

  • The language is easy to get started with.

  • ほとんどのコードは、手間をかけずに素早く作成および変更できます。

  • 書かれたコードが少なければ少ないほど、エラーや修正の誤りが少なくなります。

  • The code is easy to read (little clutter).

  • 動作テスト時にコンパイルは必要ありません。

  • ランタイムは小さいです。

  • It has duck-typing and polymorphism by nature.

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

  • 静的型付き言語よりもパフォーマンスが劣ります。

  • More difficult to refactor (symbols can't be traced).

  • 静的型付け言語のコンパイル時に通常検出されるエラーの中には、コードの実行中にのみ発生するものがあります(式の構文解析がより厳密なため)。

  • コード補完の柔軟性が低い(一部の変数型は実行時にのみ認識されます)。

This, translated to reality, means that Godot used with GDScript is a combination designed to create games quickly and efficiently. For games that are very computationally intensive and can't benefit from the engine built-in tools (such as the Vector types, Physics Engine, Math library, etc), the possibility of using C++ is present too. This allows you to still create most of the game in GDScript and add small bits of C++ in the areas that need a performance boost.

変数及び割り当て

動的に型付けされた言語の変数はすべて「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.

In GDScript, only base types (int, float, string and the vector types) are passed by value to functions (value is copied). Everything else (instances, arrays, dictionaries, etc) is passed as reference. Classes that inherit RefCounted (the default if nothing is specified) will be freed when not used, but manual memory management is allowed too if inheriting manually from 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] # You 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}
print("Name: ", d["name"], " Age: ", d["age"])

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

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

In most cases, two-dimensional arrays can often be implemented more easily with dictionaries. Here's a battleship game example:

# 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

Iterating using the C-style for loop in C-derived languages can be quite complex:

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

[..]

for (int i = 0; i < 50; i++) {
            printf("Value: %c Index: %d\n", strings[i], i);
    }

// Even in STL:
std::list<std::string> strings;

[..]

    for (std::string::const_iterator it = strings.begin(); it != strings.end(); it++) {
            std::cout << *it << std::endl;
    }

Because of this, GDScript makes the opinionated decision to have a for-in loop over iterables instead:

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 count from 0 to n in steps of 1. The parameter n is exclusive.
range(b, n) # Will count from b to n in steps of 1. The parameters b is inclusive. The parameter n is exclusive.
range(b, n, s) # Will count from b to n, in steps of s. The parameters b is inclusive. The parameter n is exclusive.

Some examples involving C-style for loops:

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

And backwards looping done through a negative counter:

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タイピングと呼ぶべきですね。

It's possible that the object being hit doesn't have a smash() function. Some dynamically typed languages simply ignore a method call when it doesn't exist, but GDScript is stricter, so checking if the function exists is desirable:

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

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