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.
Checking the stable version of the documentation...
GDScript: Вступ до мов динамічного типу
Відомості
Цей підручник має стати короткою довідкою з ефективнішого використання GDScript. Він зосереджений, в основному, на випадках, характерних для цієї мови, але також охоплює багато інформації про динамічні мови в загальному.
Ця довідка може бути особливо корисною для програмістів, у яких мало, або нема взагалі, досвіду роботи з динамічними мовами.
Динамічна природа
Плюси і мінуси динамічного типу
GDScript - це мова динамічного типу. Основними її перевагами є:
Розпочати мову легко.
Більшість коду можна записати і змінити швидко та без клопоту.
Код легко читається (невеликий безлад).
Не потрібна компіляція для тестування.
Час виконання крихітний.
За своєю природою має качиний тип і поліморфізм.
Хоча основними недоліками є:
Менша продуктивність, ніж у статичних мов.
Складніше рефакторити (символи не можна відстежити).
Деякі помилки, які, як правило, виявляються під час компіляції в статично набраних мовах, з’являються лише під час виконання коду (оскільки синтаксичний аналіз виразів є суворішим).
Менша гнучкість для завершення коду (деякі типи змінних відомі лише під час виконання).
У перекладі на реальність це означає, що 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 і векторні типи) передаються у функції за значенням (копіюються). Все інше (екземпляри, масиви, словники тощо) передається як посилання. Класи, які успадковують class_RefCounted (за замовчуванням, якщо нічого не вказано), звільняються, коли не використовуються, але ручне керування пам'яттю також допускається, якщо успадковувати вручну від class_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!")
Словники
Словники є потужним інструментом у мовах з динамічним типом. У GDScript нетипові словники можна використовувати для багатьох випадків, коли статично типізована мова має тенденцію використовувати іншу структуру даних.
Словники можуть зіставляти будь-яке значення з будь-яким іншим значенням з повним ігноруванням типу даних, який використовується як ключ, або значення. Всупереч поширеній думці, вони ефективні, оскільки їх можна реалізувати за допомогою хеш-таблиць. Насправді вони настільки ефективні, що деякі мови реалізують масиви, як словники.
Приклад словника:
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
Власні ітератори
You can create custom iterators in case the default ones don't quite meet your
needs by overriding _iter_init(), _iter_next(), and _iter_get()
functions in your script. An example implementation of a forward iterator follows:
class ForwardIterator:
var _start
var _end
var _increment
func _init(start, end, increment):
_start = start
_end = end
_increment = increment
func _should_continue(current):
return current < _end
func _iter_init(iter):
# Initialize the state to store the current value.
iter[0] = _start
return _should_continue(iter[0])
func _iter_next(iter):
iter[0] += _increment
return _should_continue(iter[0])
func _iter_get(iter):
# The state is not wrapped in an array for `_iter_get()`.
# The iteration value is the same as the state.
return iter
І його можна використовувати, як і будь-який інший ітератор:
var itr = ForwardIterator.new(0, 6, 2)
for i in itr:
print(i) # Will print 0, 2, and 4.
It is possible but discouraged to store the state in a member variable.
Multiple states are necessary in cases such as nested loops where the same
iterator instance is used simultaneously. The iter parameter in
_iter_init() and _iter_next() is a single-element array so that updates
can persist. Whereas in _iter_get(), the state is is not wrapped because it
is supposed to be read-only.
Returning true from _iter_init() and _iter_next() indicates that the
iterator is valid. Returning false will terminate the loop.
For more details see _iter_init(), _iter_next(), and _iter_get().
Качина типізація
Однією з найскладніших для розуміння концепцій при переході від статичної мови до динамічної, є качина типізація. Качина типізація робить загальний дизайн коду набагато простішим у написанні, але її робота не очевидна.
Як приклад, уявіть ситуацію, коли великий камінь котиться по тунелю, розбиваючи все на своєму шляху. Код для каменя, в статичній мові, був би таким:
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()
Потім визначте цей метод, і все, чого торкнеться камінь, можна буде розбити.