Up to date

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

GDScript: Uma introdução às linguagens dinâmicas

Sobre

Este tutorial pretende ser uma referência rápida de como usar GDScript de forma mais eficiente. Focando, assim, em casos mais comuns específicos da linguagem, mas também contém informações sobre linguagens dinâmicamente tipadas.

É destinado a ser especialmente útil para programadores com pouca ou nenhuma experiência com linguagens dinâmicamente tipadas.

Natureza dinâmica

Prós e contras da tipagem dinâmica

GDScript é uma linguagem Dinamicamente Tipada. Como tal, suas maiores vantagens são:

  • The language is easy to get started with.

  • A maioria do código pode ser escrito e modificado rapidamente sem complicações.

  • Menos código escrito significa menos erros e enganos a serem corrigidos.

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

  • Nenhuma compilação é necessária para testar.

  • Tempo de execução é bem pequeno.

  • It has duck-typing and polymorphism by nature.

Enquanto as maiores desvantagens são:

  • Menor performance que linguagens com tipagem estática.

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

  • Alguns erros que, tipicamente, seriam detectados em tempo de compilação em linguagens tipadas estaticamente aparecem apenas rodando o código (porque a análise de expressões é mais estrita).

  • Menos flexibilidade para preenchimento de código (alguns tipos de variáveis só são reconhecidos na execução).

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.

Variáveis & atribuição

Todas as variáveis em uma linguagem dinamicamente tipada são tipo-"variante". Isto significa que seus tipos não são fixos, e é modificado apenas na atribuição. Exemplos:

Estático:

int a; // Value uninitialized.
a = 5; // This is valid.
a = "Hi!"; // This is invalid.

Dinâmico:

var a # 'null' by default.
a = 5 # Valid, 'a' becomes an integer.
a = "Hi!" # Valid, 'a' changed to a string.

Como argumentos da função:

Funções também têm natureza dinâmica, o que significa que elas podem ser invocadas com argumentos diferentes, por exemplo:

Estático:

void print_value(int value) {

    printf("value is %i\n", value);
}

[..]

print_value(55); // Valid.
print_value("Hello"); // Invalid.

Dinâmico:

func print_value(value):
    print(value)

[..]

print_value(55) # Valid.
print_value("Hello") # Valid.

Ponteiros & referência:

Em linguagens estáticas como C ou C++ (e de certa medida Java e C#), existe uma distinção entre uma variável e um ponteiro/referência a uma variável. Esse último deixa o objeto ser modificado por outras funções através da passagem de uma referência ao original.

Em C# ou Java, tudo que não é um tipo embutido (int, float ou, às vezes string) é sempre um ponteiro ou uma referência. Referências são coletadas pelo coletor de lixo automaticamente, o que significa que são apagadas quando não mais usadas. Linguagens tipadas dinamicamente tendem a usar esse modelo de memória também. Alguns exemplos:

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

Arrays

Arrays em linguagens dinamicamente tipadas podem conter muitos tipos de dados mistos diferentes e são sempre dinâmicos (podem ser redimensionados a qualquer momento). Compare, por exemplo, arrays em linguagens estaticamente tipadas:

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 no 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.

Em linguagens dinamicamente tipadas, os arrays também podem ser usados como outros tipos de dados, como listas:

var array = []
array.append(4)
array.append(5)
array.pop_front()

Ou conjuntos não ordenados:

var a = 20
if a in [10, 20, 30]:
    print("We have a winner!")

Dictionaries

Os dicionários são uma ferramenta poderosa em linguagens dinamicamente tipadas. A maioria dos programadores que vêm de linguagens estaticamente tipadas (como C++ ou C#) ignoram sua existência e tornam sua vida desnecessariamente mais difícil. Esse tipo de dados geralmente não está presente nessas linguagens (ou apenas em formato limitado).

Os dicionários podem mapear qualquer valor para qualquer outro valor com total desconsideração do tipo de dados usado como chave ou valor. Ao contrário da crença popular, eles são eficientes porque podem ser implementados com tabelas de hash. Eles são, de fato, tão eficientes que algumas linguagens vão tão longe quanto implementar matrizes como dicionários.

Exemplo de Dictionary:

var d = {"name": "John", "age": 22}
print("Name: ", d["name"], " Age: ", d["age"])

Dictionaries também são dinâmicos, as chaves podem ser adicionadas ou removidas a qualquer momento a baixo custo:

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

Dicionários também podem ser usados como marcação de dados ou estruturas rápidas. Enquanto dicionários em GDScript lembram dicionários do Python, ele também suporta sintaxe e indexação no estilo Lua, o que o torna útil para escrever estados iniciais e estruturas rápidas:

# 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

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)

Tipos de dados container (arrays e dictionaries) são iteráveis. Dictionaries permitem a iteração das chaves:

for key in dict:
    print(key, " -> ", dict[key])

Também é possível iterar com índices:

for i in range(strings.size()):
    print(strings[i])

A função range() pode ter 3 argumentos:

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

Traduzir para:

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

Torna-se:

for i in range(10, 0, -1):
    pass

Enquanto

Loops while() são os mesmos em todos os lugares:

var i = 0

while i < strings.size():
    print(strings[i])
    i += 1

Iteradores personalizados

Você pode criar iteradores personalizados caso os existentes não satisfaçam suas necessidades, sobrescrevendo as funções _iter_init, _iter_next e _iter_get da classe Variant em seu script. Aqui está um exemplo de implementação de um iterador avançado:

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

E pode ser usado como qualquer outro iterador:

var itr = ForwardIterator.new(0, 6, 2)
for i in itr:
    print(i) # Will print 0, 2, and 4.

Certifique-se de redefinir o estado do iterador em _iter_init, caso contrário, loops for aninhados que usam iteradores personalizados não funcionarão como esperado.

Duck typing

Um dos conceitos mais difíceis de entender quando passa de uma linguagem estaticamente tipada para uma dinâmica é duck typing. Duck typing faz com que o design geral do código seja mais simples e direto de escrever, mas não é óbvio como ele funciona.

Como exemplo, imagine uma situação onde uma grande rocha está caindo de um túnel, esmagando tudo em seu caminho. O código para a rocha, em uma linguagem estaticamente tipada seria algo como:

void BigRollingRock::on_object_hit(Smashable *entity) {

    entity->smash();
}

Desta forma, tudo o que pode ser esmagado por uma rocha teria que herdar Smashable. Se um personagem, um inimigo, uma peça de mobília, uma pequena pedra fossem todos quebráveis, eles precisariam herdar da classe Smashable, possivelmente exigindo herança múltipla. Se herança múltipla fosse indesejada, eles teriam que herdar uma classe comum como Entity. No entanto, não seria muito elegante adicionar um método virtual smash() à Entity apenas se alguns deles puderem ser destruídos.

Com linguagens dinamicamente tipadas, isso não é um problema. Duck typing garante que você só precisa definir uma função smash() onde for necessário e pronto. Não há necessidade de considerar herança, classes base, etc.

func _on_object_hit(object):
    object.smash()

E é isso. Se o objeto que atingiu a pedra grande tiver um método smash (), ele será chamado. Não há necessidade de herança ou polimorfismo. Linguagens dinamicamente tipadas só se preocupam com a instância que possui o método ou membro desejado, não o que ele herda ou o tipo de classe. A definição de Duck Typing deve tornar isso mais claro:

"Quando eu vejo um pássaro que anda como um pato e nada como um pato e grasna como um pato, eu chamo esse pássaro de pato"

Neste caso, traduz para:

"Se o objeto pode ser esmagado, não importa o que seja, apenas esmague-o."

Sim, nós deveríamos chamar isso de tipo Hulk ao invés disso.

It's possible that the object being hit doesn't have a smash() function. Some dynamically typed languages simply ignore a method call when it doesn't exist, but GDScript is stricter, so checking if the function exists is desirable:

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

Então, simplesmente defina esse método e qualquer coisa que a rocha tocar pode ser destruída.