Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

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 :

  • The language is easy to get started with.

  • La plupart du code peut être écrit et modifié rapidement et sans tracas.

  • Moins de code écrit signifie moins d'erreurs à corriger.

  • The code is easy to read (little clutter).

  • Aucune compilation n'est nécessaire pour tester.

  • Le temps d'exécution est minuscule.

  • It has duck-typing and polymorphism by nature.

Alors que les principaux inconvénients sont :

  • Moindre performance que les langages statiquement typés.

  • More difficult to refactor (symbols can't be traced).

  • 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).

  • Moins de flexibilité pour la complétion de code (certains types de variables ne sont connus qu'au moment de l'exécution).

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.

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.

In GDScript, only base types (int, float, string and the vector types) are passed by value to functions (value is copied). Everything else (instances, arrays, dictionaries, etc) is passed as reference. Classes that inherit RefCounted (the default if nothing is specified) will be freed when not used, but manual memory management is allowed too if inheriting manually from 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

Les dictionnaires sont un outil très puissant dans les langages dynamiquement typés. La plupart des programmeurs qui viennent de langages typés statiquement (comme C++ ou C#) ignorent leur existence et se rendent la vie inutilement plus difficile. Ce type de données n'est généralement pas présent dans ces langages (ou seulement sous une forme limitée).

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.

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

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

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)

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.

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) {}

Se traduit en :

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--) {}

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()

Ensuite, il suffit de définir cette méthode et tout ce que la roche touche peut être écrasé.