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 : Une introduction aux langages dynamiques
À propos
Ce tutoriel a pour but d'être une référence rapide pour savoir comment utiliser GDScript plus efficacement. Il se concentre sur les cas communs spécifiques au langage, mais couvre aussi beaucoup d'informations sur les langues à typage dynamique.
Il est destiné à être particulièrement utile pour les programmeurs ayant peu ou pas d'expérience préalable avec les langages à typage dynamique.
Nature dynamique
Avantages et inconvénients du typage dynamique
GDScript est un langage à typage dynamique. En tant que tel, ses principaux avantages sont que :
Le langage est simple et facile à apprendre.
La plupart du code peut être écrit et modifié rapidement et sans tracas.
Le code est facile à lire (peu d'encombrement).
Aucune compilation n'est nécessaire pour tester.
Le temps d'exécution est minuscule.
Typage ad hoc et polymorphisme par nature.
Alors que les principaux inconvénients sont :
Moindre performance que les langages statiquement typés.
Plus difficile à refactoriser (les symboles ne peuvent pas être tracés).
Certaines erreurs qui seraient typiquement détectées au moment de la compilation dans des langues typées statiquement n'apparaissent que lors de l'exécution du code (parce que l'analyse des expressions est plus stricte).
Less flexibility for code-completion (some variable types are only known at runtime).
En pratique, cela signifie que Godot utilisé avec GDScript est une combinaison conçue pour créer des jeux rapidement et efficacement. Pour les jeux qui nécessitent beaucoup de calculs et ne peuvent pas bénéficier des outils intégrés au moteur (tels que les types Vector, Physics Engine, Math library, etc.), la possibilité d'utiliser C++ est également présente. Cela vous permet de continuer à créer la plupart du jeu en GDScript et d'ajouter de petits morceaux de C++ dans les zones qui nécessitent un boost de performances.
Variables et affectation
Toutes les variables d'un langage typé dynamiquement sont de type "variant". Cela signifie que leur type n'est pas fixe et n'est modifié que par l'affectation. Exemple :
Statique :
int a; // Value uninitialized.
a = 5; // This is valid.
a = "Hi!"; // This is invalid.
Dynamique :
var a # 'null' by default.
a = 5 # Valid, 'a' becomes an integer.
a = "Hi!" # Valid, 'a' changed to a string.
En tant qu'arguments de fonctions :
Les fonctions sont également de nature dynamique, ce qui signifie qu'elles peuvent être appelées avec différents arguments, par exemple :
Statique :
void print_value(int value) {
printf("value is %i\n", value);
}
[..]
print_value(55); // Valid.
print_value("Hello"); // Invalid.
Dynamique :
func print_value(value):
print(value)
[..]
print_value(55) # Valid.
print_value("Hello") # Valid.
Pointeurs et référencement :
Dans les langages statiques tels que C ou C++ (et dans une certaine mesure Java et C#), il y a une distinction entre une variable et un pointeur/référence à une variable. Ce dernier permet à l'objet d'être modifié par d'autres fonctions en passant une référence de l'objet original.
En C# ou Java, tout ce qui n'est pas un type intégré (int, float, parfois String) est toujours un pointeur ou une référence. Les références sont également collectées par le ramasse-miette automatiquement, ce qui signifie qu'elles sont effacées lorsqu'elles ne sont plus utilisées. Les langages à typage dynamique ont aussi tendance à utiliser ce modèle de mémoire. Quelques exemples :
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.
Dans GDScript, seuls les types de base (int, float, string et les types vectoriels) sont transmis par valeur aux fonctions (la valeur est copiée). Tout le reste (instances, tableaux, dictionnaires, etc.) est passé par référence. Les classes qui héritent de RefCounted (par défaut si rien n'est spécifié) seront libérées lorsqu'elles ne sont pas utilisées, mais la gestion manuelle de la mémoire est également autorisée si elles héritent manuellement de Object.
Les tableaux
Les tableaux en langages typées dynamiquement peuvent contenir de nombreux types de données mixtes différents à l'intérieur et sont toujours dynamiques (redimensionnables à tout moment). Comparez par exemple les tableaux dans des langages typées statiquement :
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.
Et en 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.
Dans les langages typées dynamiquement, les tableaux peuvent aussi servir comme autres types de données, comme les listes :
var array = []
array.append(4)
array.append(5)
array.pop_front()
Ou des ensembles non ordonnés :
var a = 20
if a in [10, 20, 30]:
print("We have a winner!")
Les dictionnaires
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.
Les dictionnaires peuvent associer n'importe quelle valeur à n'importe quelle autre valeur sans tenir compte du type de données utilisé comme clé ou valeur. Contrairement à la croyance populaire, ils sont très efficaces car ils peuvent être implémentés avec des tables de hachage. Ils sont, en fait, si efficaces que certains langages iront jusqu'à implémenter les tableaux comme des dictionnaires.
Exemple de dictionnaire :
var d = {"name": "John", "age": 22}
print("Name: ", d["name"], " Age: ", d["age"])
Les dictionnaires sont également dynamiques, les clés peuvent être ajoutées ou supprimées à tout moment et à faible coût :
d["mother"] = "Rebecca" # Addition.
d["age"] = 11 # Modification.
d.erase("name") # Removal.
Dans la plupart des cas, les tableaux bidimensionnels peuvent souvent être implémentés plus facilement avec des dictionnaires. Voici un exemple de jeu de bataille navale simple :
# 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))
Les dictionnaires peuvent également être utilisés comme balises de données ou comme structures rapides. Bien que les dictionnaires GDScript ressemblent aux dictionnaires python, ils supportent également la syntaxe et l'indexation de style Lua, ce qui le rend très utile pour écrire des états initiaux et des structures rapides :
# 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.
Boucles for et while
L'itération à l'aide de la boucle for dans les langages dérivés du C peut être assez complexe :
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;
}
De ce fait, GDScript a pris la décision avisée d'avoir une boucle for-in sur les itérables à la place :
for s in strings:
print(s)
Les types de données de conteneurs (tableaux et dictionnaires) sont itérables. Les dictionnaires permettent d'itérer les clés :
for key in dict:
print(key, " -> ", dict[key])
L'itération avec les indices est également possible :
for i in range(strings.size()):
print(strings[i])
La fonction range() peut prendre 3 arguments :
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.
Quelques exemples impliquant des boucles for dans le style C :
for (int i = 0; i < 10; i++) {}
for (int i = 5; i < 10; i++) {}
for (int i = 5; i < 10; i += 2) {}
Se traduit en :
for i in range(10):
pass
for i in range(5, 10):
pass
for i in range(5, 10, 2):
pass
Et l'itération à rebours se fait à l'aide d'un compteur négatif :
for (int i = 10; i > 0; i--) {}
Devient :
for i in range(10, 0, -1):
pass
While
les boucles while() sont les mêmes partout :
var i = 0
while i < strings.size():
print(strings[i])
i += 1
Itérateurs personnalisés
Vous pouvez créer des itérateurs personnalisés dans le cas où ceux par défaut ne répondent pas tout à fait à vos besoins, en redéfinissant les fonctions de la classe Variant _iter_init
, _iter_next
, et _iter_get
dans votre script. Voici un exemple d'implémentation d'un tel itérateur :
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
Et il peut être utilisé comme n'importe quel autre itérateur :
var itr = ForwardIterator.new(0, 6, 2)
for i in itr:
print(i) # Will print 0, 2, and 4.
Assurez-vous de réinitialiser l'état de l'itérateur dans _iter_init
, sinon les boucles imbriquées qui utilisent des itérateurs personnalisés ne fonctionneront pas comme prévu.
Le typage canard (duck typing)
L'un des concepts les plus difficiles à saisir lorsqu'on passe d'un langage typé statiquement à un langage dynamique est le duck typing. Ce typage ad hoc rend la conception globale du code beaucoup plus simple et directe à écrire, mais il n'est pas évident de savoir comment cela fonctionne.
Par exemple, imaginez une situation où un gros rocher tombe dans un tunnel, détruisant tout sur son passage. Le code pour le rocher, dans un langage typé statiquement, serait quelque chose comme :
void BigRollingRock::on_object_hit(Smashable *entity) {
entity->smash();
}
De cette façon, tout ce qui peut être brisé par une pierre devrait hériter de Smashable. Si un personnage, un ennemi, un meuble, un meuble, une petite pierre étaient tous écrasables, ils auraient besoin d'hériter de la classe Smashable, ce qui pourrait nécessiter un héritage multiple. Si l'héritage multiple n'était pas désiré, alors ils devraient hériter d'une classe commune comme Entity. Pourtant, il ne serait pas très élégant d'ajouter une méthode virtuelle smash()
à Entity seulement si quelques-uns d'entre eux peuvent être écrasés.
Avec les langages de typage dynamique, ce n'est pas un problème. Le typage canard permet de s'assurer que vous n'avez qu'à définir une fonction smash()
là où c'est nécessaire et c'est tout. Pas besoin de considérer l'héritage, les classes de base, etc.
func _on_object_hit(object):
object.smash()
Et c'est tout. Si l'objet qui a frappé le gros rocher a une méthode smash(), il sera appelé. Pas besoin d'héritage ou de polymorphisme. Les langues typées dynamiquement ne se soucient que de l'instance ayant la méthode ou le membre désiré, et non de ce dont elle hérite ou du type de classe. La définition du Duck Typing devrait rendre cela plus clair :
"Quand je vois un oiseau qui marche comme un canard et nage comme un canard et cancane comme un canard, j'appelle cet oiseau un canard."
Dans ce cas, cela se traduit par :
"Si l'objet peut être écrasé, peu importe ce que c'est, écrasez-le."
Oui, on devrait plutôt l'appeler le typage à la Hulk.
Il est possible que l'objet touché n'ait pas de fonction smash(). Certains langages à typage dynamique ignorent simplement un appel de méthode lorsqu'il n'existe pas, mais GDScript est plus strict, il est donc souhaitable de vérifier si la fonction existe :
func _on_object_hit(object):
if object.has_method("smash"):
object.smash()
Then, define that method and anything the rock touches can be smashed.