Up to date

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

GDScript: Введение в динамически типизированные языки

О Godot Engine

Это руководство призвано стать кратким руководством по более эффективному использованию 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.

Переменные и присваивание

Все переменные в динамически типизированном языке являются "вариантными" типами. Это означает, что их тип не является фиксированным, а модифицируется только через присваивание. Пример:

Статическая типизация:

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.
}
  • GDScriрt:

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!")

Словари (Dictionaries)

Словари являются мощным инструментом в динамически типизированных языках. Большинство программистов из статически типизированных языков (таких как 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() везде одинаковые:

var i = 0

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

Пользовательские итераторы

Вы можете создавать собственные итераторы в случае, если стандартные по умолчанию не вполне соответствуют вашим потребностям, переопределив функции _iter_init, _iter_next и _iter_get класса Variant в вашем скрипте. Ниже приведен пример реализации итератора прямого действия:

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-циклы, использующие пользовательские итераторы не будут работать, как ожидалось.

Утиная типизация

Одной из самых сложных концепций, которую нужно понять при переходе от языка со статической типизацией к динамической, является Утиная типизация. Утиная типизация делает общий дизайн кода намного проще и понятнее для написания, но для новичка это может быть не так очевидно.

В качестве примера представим ситуацию, когда большой камень падает по туннелю, разбивая все на своем пути. Код для камня на статически типизированном языке будет выглядеть примерно так:

void BigRollingRock::on_object_hit(Smashable *entity) {

    entity->smash();
}

Таким образом, все, что может быть разбито камнем, должно наследовать Smashable. Если персонаж, враг, предмет мебели, маленький камень - все это можно разбить, им будет необходимо наследовать от класса Smashable, возможно, требуя множественного наследования. Если множественное наследование нежелательно, им придется наследовать общий класс, такой как Entity. Тем не менее, было бы не очень элегантно добавить виртуальный метод smash() в Entity, только если некоторые из них могут быть разбиты.

С динамически типизированными языками это не проблема. Утиная типизация гарантирует, что вам нужно только определить функцию smash() там, где требуется, и все. Не нужно учитывать наследование, базовые классы и т.д.

func _on_object_hit(object):
    object.smash()

И это все. Если объект, столкнувшийся с камнем, имеет метод smash(), он будет вызван. Нет необходимости наследования или полиморфизма. Языки с динамической типизацией заботятся только о том, чтобы экземпляр имел нужный метод или член, а не о том, что он наследует, или о типе класса. Определение Утиной типизации должно прояснить это:

"Когда Я вижу птицу, которая ходит как утка и плавает как утка, и крякает как утка, Я называю эту птицу уткой"

В этом случае это переводится как:

"Если объект может быть разбит, все равно, что это, просто разбейте его."

Да, возможно нам следует называть это типизацией Халка.

Возможно, поражаемый объект не имеет функции smash(). Некоторые языки с динамической типизацией просто игнорируют вызов метода, если он не существует, но GDScript более строгий, поэтому желательно проверить, существует ли функция:

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

Затем просто определите этот метод, и все, к чему прикасается камень, может быть разбито.