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: Un'introduzione ai linguaggi dinamici
Informazioni
Questo tutorial punta a essere un rapido riferimento su come utilizzare GDScript in modo più efficiente. Si concentra sui casi comuni specifici del linguaggio, ma copre anche molte informazioni sui linguaggi a tipizzazione dinamica.
È pensato per essere particolarmente utile per i programmatori con poca o nessuna esperienza precedente con i linguaggi dinamicamente tipizzati.
Natura dinamica
Pro & contro della tipizzazione dinamica
GDScript è un linguaggio dinamicamente tipizzato. Come tale, i suoi principali vantaggi sono che:
È facile iniziare con il linguaggio.
La maggior parte del codice si può scrivere e cambiare rapidamente e senza problemi.
Il codice è facile da leggere (poco ingombrante).
Non è richiesta alcuna compilazione per provarlo.
Il codice in esecuzione è minuscolo.
È per sua natura caratterizzato da duck-typing e polimorfismo.
Mentre i principali svantaggi sono:
Prestazioni inferiori rispetto ai linguaggi staticamente tipizzati.
Più difficile da rifattorizzare (i simboli non possono essere tracciati).
Alcuni errori che normalmente verrebbero rilevati in fase di compilazione nei linguaggi staticamente tipizzati si verificano solamente durante l'esecuzione del codice (perché l'analisi delle espressioni è più rigorosa).
Minore flessibilità per il completamento del codice (alcuni tipi di variabili sono noti solo in fase di esecuzione).
Tradotto in pratica, questo significa che Godot, utilizzato con GDScript, è una combinazione progettata per creare giochi in modo rapido ed efficiente. Per i giochi che richiedono calcoli molto intensivi e non possono beneficiare degli strumenti integrati nel motore (come i tipi di vettori, il motore di fisica, la libreria di matematica, ecc.), c'è anche la possibilità di utilizzare il C++. Questo permette di creare la maggior parte del gioco in GDScript e di aggiungere piccole porzioni di C++ nelle aree che hanno bisogno di migliori prestazioni.
Variabili e assegnazione
Tutte le variabili in un linguaggio a tipizzazione dinamica sono di tipo "variant". Ciò significa che il loro tipo non è fisso e viene modificato solo tramite assegnazione. Esempio:
Statico:
int a; // Value uninitialized.
a = 5; // This is valid.
a = "Hi!"; // This is invalid.
Dinamico:
var a # 'null' by default.
a = 5 # Valid, 'a' becomes an integer.
a = "Hi!" # Valid, 'a' changed to a string.
Come argomenti di funzioni:
Anche le funzioni sono di natura dinamica, il che significa che si possono chiamare con argomenti diversi, ad esempio:
Statico:
void print_value(int value) {
printf("value is %i\n", value);
}
[..]
print_value(55); // Valid.
print_value("Hello"); // Invalid.
Dinamico:
func print_value(value):
print(value)
[..]
print_value(55) # Valid.
print_value("Hello") # Valid.
Puntatori e riferimenti:
Nei linguaggi statici, come C o C++ (e fino a un certo punto anche Java e C#), esiste una distinzione tra una variabile e un puntatore/riferimento a una variabile. Quest'ultimo consente di modificare l'oggetto da altre funzioni passando un riferimento all'oggetto originale.
In C# o Java, tutto ciò che non è un tipo integrato (int, float, talvolta String) è sempre un puntatore o un riferimento. Inoltre, i riferimenti sono automaticamente ripuliti dal garbage collector, il che significa che vengono cancellati quando non sono più in uso. Anche i linguaggi a tipizzazione dinamica tendono a utilizzare questo modello di memoria. Alcuni esempi:
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, solo i tipi base (int, float, string e i tipi di vettore) sono passati per valore alle funzioni (il valore è copiato). Tutto il resto (istanze, array, dizionari, ecc.) è passato per riferimento. Le classi che ereditano RefCounted (la classe predefinita se nulla viene specificato) verranno liberate se non utilizzate, ma è consentito anche gestire manualmente la memoria se si eredita manualmente da Object.
Array
Gli array nei linguaggi a tipizzazione dinamica possono contenere al loro interno molti tipi diversi di dati misti e sono sempre dinamici (si possono ridimensionare in qualsiasi momento). Si confrontino, ad esempio, gli array nei linguaggi a tipizzazione statica:
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.
E in 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.
Nei linguaggi a tipizzazione dinamica, gli array possono anche fungere da altri tipi di dati, come liste:
var array = []
array.append(4)
array.append(5)
array.pop_front()
O insiemi non ordinati:
var a = 20
if a in [10, 20, 30]:
print("We have a winner!")
Dizionari
I dizionari sono uno strumento potente nei linguaggi dinamicamente tipizzati. In GDScript, i dizionari non tipizzati possono servire in molti casi in cui un linguaggio staticamente tipizzato tenderebbe a utilizzare una struttura dati diversa.
I dizionari possono mappare qualsiasi valore a qualsiasi altro valore, senza tenere conto del tipo di dato utilizzato come chiave o valore. Contrariamente a quanto si pensa, sono efficienti perché si possono implementare con tabelle hash. Sono, infatti, così efficienti che alcuni linguaggi arrivano persino a implementare i propri array come dizionari.
Esempio di dizionario:
var d = {"name": "John", "age": 22}
print("Name: ", d["name"], " Age: ", d["age"])
Anche i dizionari sono dinamici: le chiavi si possono aggiungere o rimuovere in qualsiasi momento a basso costo:
d["mother"] = "Rebecca" # Addition.
d["age"] = 11 # Modification.
d.erase("name") # Removal.
Nella maggior parte dei casi, gli array bidimensionali si possono implementare più facilmente con i dizionari. Ecco un esempio di gioco di battaglia navale:
# 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))
I dizionari possono anche servire da markup di dati o strutture rapide. Sebbene i dizionari di GDScript assomiglino ai dizionari di Python, supportano anche la sintassi e l'indicizzazione in stile Lua, il che li rende utili per scrivere stati iniziali e strutture rapide:
# 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 e while
L'iterazione tramite il ciclo for in stile C nei linguaggi derivati da C può essere piuttosto complessa:
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;
}
Pertanto, GDScript ha preso la opinabile decisione di avere un ciclo for-in sugli iterabili:
for s in strings:
print(s)
I tipi di dati contenitore (array e dizionari) sono iterabili. I dizionari permettono di iterare le chiavi:
for key in dict:
print(key, " -> ", dict[key])
È anche possibile iterare con gli indici:
for i in range(strings.size()):
print(strings[i])
La funzione range() può accettare 3 argomenti:
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.
Alcuni esempi riguardo i cicli for in stile C:
for (int i = 0; i < 10; i++) {}
for (int i = 5; i < 10; i++) {}
for (int i = 5; i < 10; i += 2) {}
Si traduce in:
for i in range(10):
pass
for i in range(5, 10):
pass
for i in range(5, 10, 2):
pass
E un ciclo all'indietro eseguito tramite un contatore negativo:
for (int i = 10; i > 0; i--) {}
Diventa:
for i in range(10, 0, -1):
pass
Mentre
I cicli while() sono gli stessi ovunque:
var i = 0
while i < strings.size():
print(strings[i])
i += 1
Iteratori personalizzati
È possibile creare iteratori personalizzati nel caso in cui quelli predefiniti non soddisfino appieno le proprie esigenze, sovrascrivendo le funzioni _iter_init, _iter_next e _iter_get nel proprio script. Di seguito un esempio di implementazione di un iteratore in avanti:
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
E si può utilizzare come qualsiasi altro iteratore:
var itr = ForwardIterator.new(0, 6, 2)
for i in itr:
print(i) # Will print 0, 2, and 4.
È possibile, ma sconsigliato, memorizzare lo stato in una variabile membro. Sono necessari più stati in casi come i cicli annidati, dove la stessa istanza dell'iteratore è utilizzata simultaneamente. Il parametro iter in _iter_init() e _iter_next() è un array con un singolo elemento in modo che gli aggiornamenti possano persistere. Invece, in _iter_get(), lo stato non viene incapsulato perché dovrebbe essere di sola lettura.
Restituire true da _iter_init() e _iter_next() indica che l'iteratore è valido. Restituire false interromperà il ciclo.
Per maggiori dettagli, consultare _iter_init(), _iter_next() e _iter_get().
Tipizzazione dinamica (duck typing)
Uno dei concetti più difficili da comprendere quando si passa da un linguaggio staticamente tipizzato a uno dinamico è il duck typing. Il duck typing semplifica notevolmente la progettazione del codice e la rende più immediata da scrivere, ma il suo funzionamento non è così ovvio.
Ad esempio, ci si può immaginare una situazione in cui un grosso masso cade in un tunnel, distruggendo tutto ciò che incontra sul suo cammino. Il codice per il masso, in un linguaggio staticamente tipizzato, sarebbe qualcosa come:
void BigRollingRock::on_object_hit(Smashable *entity) {
entity->smash();
}
In questo modo, tutto ciò che può essere distrutto da un masso dovrebbe ereditare Smashable. Se un personaggio, un nemico, un mobile, una pietruzza fossero tutti distruttibili, dovrebbero ereditare dalla classe Smashable, il che potrebbe necessitare di ereditarietà multipla. Se l'ereditarietà multipla non fosse desiderata, allora dovrebbero ereditare una classe comune come Entity. Tuttavia, non sarebbe molto elegante aggiungere un metodo virtuale smash() a Entity solamente se alcuni di essi si possono distruggere.
Con i linguaggi a tipizzazione dinamica, questo non è un problema. Il duck typing assicura che basti definire una funzione smash() solo dove necessario, e tutto qui. Non c'è bisogno di considerare ereditarietà, classi padre, ecc.
func _on_object_hit(object):
object.smash()
E questo è tutto. Se l'oggetto che ha colpito il grosso masso ha un metodo smash(), verrà chiamato. Non c'è bisogno di ereditarietà o polimorfismo. I linguaggi dinamicamente tipizzati si preoccupano solo che l'istanza abbia il metodo o il membro desiderato, non di ciò che eredita o del tipo di classe. La definizione di Duck Typing dovrebbe chiarire questo punto:
"Quando vedo un uccello che cammina come un'anatra, nuota come un'anatra e starnazza come un'anatra, chiamo quell'uccello "anatra"
In questo caso, si traduce in:
"Se l'oggetto si può distruggere, non importa cosa sia, distruggilo e basta."
Si, si dovrebbe chiamare Hulk Typing invece.
È possibile che l'oggetto colpito non abbia una funzione smash(). Alcuni linguaggi a tipizzazione dinamica ignorano semplicemente una chiamata a un metodo quando non esiste, ma GDScript è più restrittivo, quindi è opportuno verificare se la funzione esiste:
func _on_object_hit(object):
if object.has_method("smash"):
object.smash()
Successivamente, definire quel metodo. Ora tutto ciò che il masso tocca si può distruggere.