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.

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

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

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

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

  • Менша гнучкість для заповнення коду (деякі типи змінних стають відомі лише під час виконання).

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

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

Словники

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

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

Потім просто визначте цей метод і все, до чого торкатиметься камінь, може бути розбито.