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.

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:

  • A linguagem é fácil de começar a usar.

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

  • O código é fácil de ler (pouca poluição).

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

  • Tempo de execução é bem pequeno.

  • Possui tipagem pato (duck-typing) e polimorfismo por natureza.

Enquanto as maiores desvantagens são:

  • Menor performance que linguagens com tipagem estática.

  • Mais difícil de refatorar (os símbolos não podem ser rastreados).

  • 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 autocompletar código (alguns tipos de variáveis só são conhecidos em tempo de execução).

Isso, traduzido para a realidade, significa que o Godot usado com o GDScript é uma combinação projetada para criar jogos de forma rápida e eficiente. Para jogos que são muito intensivos computacionalmente e não podem se beneficiar das ferramentas integradas da engine (como os tipos Vector, Engine de Física, biblioteca de Matemática, etc), a possibilidade de usar C++ também está presente. Isso permite que você ainda crie a maior parte do jogo em GDScript e adicione pequenos trechos de C++ nas áreas que precisam de um aumento de desempenho.

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.

No GDScript, apenas os tipos básicos (int, float, string e os tipos vector) são passados por valor para as funções (o valor é copiado). Todo o resto (instâncias, arrays, dicionários, etc.) é passado por referência. Classes que herdam de RefCounted (o padrão se nada for especificado) serão liberadas quando não estiverem em uso, mas o gerenciamento manual de memória também é permitido se herdado manualmente de 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!")

Dicionários

Dicionários são uma ferramenta poderosa em linguagens dinamicamente tipadas. No GDScript, dicionários não tipados podem ser usados em muitos casos onde uma linguagem estaticamente tipada tenderia a usar outra estrutura de dados.

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.

Na maioria dos casos, arrays bidimensionais podem frequentemente ser implementados de forma mais fácil com dicionários. Aqui está um exemplo de jogo de batalha naval:

# 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

Iterar usando o loop for no estilo C em linguagens derivadas de C pode ser bastante complexo:

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;
}

Por causa disso, o GDScript toma a decisão opinativa de ter um loop for-in sobre iteráveis:

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

The range() function can take 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.

Alguns exemplos envolvendo loops for no estilo C:

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

E o loop reverso feito através de um contador negativo:

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 padrões não atendam totalmente às suas necessidades, sobrescrevendo as funções _iter_init(), _iter_next() e _iter_get() no seu script. Uma implementação de exemplo de um iterador progressivo segue abaixo:

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

É possível, mas desencorajado, armazenar o estado em uma variável de membro. Múltiplos estados são necessários em casos como loops aninhados onde a mesma instância do iterador é usada simultaneamente. O parâmetro iter em _iter_init() e _iter_next() é um array de um único elemento para que as atualizações possam persistir. Enquanto em _iter_get(), o estado não é encapsulado porque supõe-se que seja apenas para leitura.

Retornar true em _iter_init() e _iter_next() indica que o iterador é válido. Retornar false encerrará o loop.

Para mais detalhes, veja _iter_init(), _iter_next(), e _iter_get().

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.

É possível que o objeto atingido não tenha uma função smash(). Algumas linguagens de tipagem dinâmica simplesmente ignoram uma chamada de método quando ele não existe, mas o GDScript é mais estrito, então verificar se a função existe é desejável:

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

Depois, defina esse método e qualquer coisa que a rocha tocar poderá ser esmagada.