Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

GDScript: Вступ до мов динамічного типу

Відомості

Цей підручник має стати короткою довідкою з ефективнішого використання GDScript. Він зосереджений, в основному, на випадках, характерних для цієї мови, але також охоплює багато інформації про динамічні мови в загальному.

Ця довідка може бути особливо корисною для програмістів, у яких мало, або нема взагалі, досвіду роботи з динамічними мовами.

Динамічна природа

Плюси і мінуси динамічного типу

GDScript - це мова динамічного типу. Основними її перевагами є:

  • Розпочати мову легко.

  • Більшість коду можна записати і змінити швидко та без клопоту.

  • Код легко читається (невеликий безлад).

  • Не потрібна компіляція для тестування.

  • Час виконання крихітний.

  • За своєю природою має качиний тип і поліморфізм.

Хоча основними недоліками є:

  • Менша продуктивність, ніж у статичних мов.

  • Складніше рефакторити (символи не можна відстежити).

  • Деякі помилки, які, як правило, виявляються під час компіляції в статично набраних мовах, з’являються лише під час виконання коду (оскільки синтаксичний аналіз виразів є суворішим).

  • Less flexibility for code-completion (some variable types are only known at runtime).

У перекладі на реальність це означає, що Godot, який використовується з GDScript, є комбінацією, призначеною для швидкого та ефективного створення ігор. Для ігор, які потребують великої кількості обчислень і не можуть отримати переваги від вбудованих інструментів механізму (таких як векторні типи, фізичний механізм, математична бібліотека тощо), також присутня можливість використання C++. Це дозволяє вам усе ще створювати більшу частину гри в GDScript і додавати невеликі фрагменти C++ у ті області, які потребують підвищення продуктивності.

Змінні та призначення

Усі змінні в динамічних мовах мають "невизначений" тип. Це означає, що їх тип не є фіксованим, а змінюється в момент призначення. Приклад:

Статичний:

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.
}
  • ГДСкрипт:

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 і векторні типи) передаються у функції за значенням (копіюються). Все інше (екземпляри, масиви, словники тощо) передається як посилання. Класи, які успадковують RefCounted (за замовчуванням, якщо нічого не вказано), звільняються, коли не використовуються, але ручне керування пам'яттю також допускається, якщо успадковувати вручну від 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 are a powerful tool in dynamically typed languages. In GDScript, untyped dictionaries can be used for many cases where a statically typed language would tend to use another data structure.

Словники можуть зіставляти будь-яке значення з будь-яким іншим значенням з повним ігноруванням типу даних, який використовується як ключ, або значення. Всупереч поширеній думці, вони ефективні, оскільки їх можна реалізувати за допомогою хеш-таблиць. Насправді вони настільки ефективні, що деякі мови реалізують масиви, як словники.

Приклад словника:

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.

У більшості випадків двовимірні масиви часто можна легше реалізувати за допомогою словників. Ось приклад гри про бойовий корабель:

# 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

Ітерація за допомогою циклу for у стилі C у мовах, похідних від C, може бути досить складною:

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;
}

Через це GDScript приймає впевнене рішення використовувати цикл for-in замість ітерацій:

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.

Деякі приклади циклів for у стилі C:

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() цикли всюди однакові:

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(). Деякі динамічно набрані мови просто ігнорують виклик методу, коли його не існує (наприклад, Objective C), але GDScript суворіший, тому бажано перевірити, чи функція існує:

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

Then, define that method and anything the rock touches can be smashed.