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 é muito simples de aprender.
  • 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.
  • Mais fácil de ler o código (menos bagunça).
  • Nenhuma compilação é necessária para testar.
  • Tempo de execução é bem pequeno.
  • Polimorfismo e tipagem pato naturalmente.

Enquanto as maiores desvantagens são:

  • Menor performance que linguagens com tipagem estática.
  • Mais difícil de refatorar (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 preenchimento de código (alguns tipos de variáveis só são reconhecidos na execução).

This, translated to reality, means that Godot+GDScript are 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:

In static languages, such as C or C++ (and to some extent Java and C#), there is a distinction between a variable and a pointer/reference to a variable. The latter allows the object to be modified by other functions by passing a reference to the original one.

In C# or Java, everything not a built-in type (int, float, sometimes String) is always a pointer or a reference. References are also garbage-collected automatically, which means they are erased when no longer used. Dynamically typed languages tend to use this memory model, too. Some Examples:

  • 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, somente os tipos básicos (int, float, string e os tipos vetores) são passados por valor para funções (o valor é copiado). Todo o resto (instances, arrays, dictionaries, etc) é passado como referência. As classes que herdam: ref: class_Reference (o padrão se nada for especificado) serão liberadas quando não usadas, mas o gerenciamento de memória manual também é permitido se herdar manualmente de: ref:` class_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] # Simple, and 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

Dictionaries are a powerful tool in dynamically typed languages. Most programmers that come from statically typed languages (such as C++ or C#) ignore their existence and make their life unnecessarily more difficult. This datatype is generally not present in such languages (or only in limited form).

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} # Simple syntax
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, muitas vezes podem ser implementados mais facilmente com dictionaries. Aqui está um exemplo de jogo de batalha naval simples:

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

Dictionaries can also be used as data markup or quick structures. While GDScript’s dictionaries resemble python dictionaries, it also supports Lua style syntax and indexing, which makes it useful for writing initial states and quick structs:

# 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 em algumas linguagens estaticamente tipadas pode ser bastante complexo:

const char* strings = new const char*[50];

[..]

for (int i = 0; i < 50; i++)
{

    printf("Value: %s\n", i, strings[i]);
}

// Even in STL:

for (std::list<std::string>::const_iterator it = strings.begin(); it != strings.end(); it++) {

    std::cout << *it << std::endl;
}

Isto é geralmente bastante simplificado em linguagens tipificadas dinamicamente:

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 go from 0 to n-1
range(b, n) # Will go from b to n-1
range(b, n, s) # Will go from b to n-1, in steps of s

Some statically typed programming language examples:

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 a repetição inversa é feita através de um contador negativo:

for (int i = 10; i > 0; i--) {}

Torna-se:

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

While

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. O tipo Duck 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 (like Objective C), 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.