Up to date

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

GDScript: Introducción a los lenguajes dinámicos

Acerca de

Este tutorial pretende ser una referencia rápida para aprender a usar GDScript de forma más eficiente. Está enfocado a casos específicos del lenguaje, pero también cubre bastante información sobre lenguajes dinámicamente tipados.

Está pensada para ser especialmente útil para programadores con poca o ninguna experiencia con lenguajes de tipado dinámico.

Naturaleza dinámica

Pros y contras del tipado dinámico

GDScript es un lenguaje de tipado dinámico. Como tal, sus grandes ventajas son:

  • The language is easy to get started with.

  • La mayoría del código puede ser escrito y cambiado rápidamente sin complicaciones.

  • Escribir menos código significa tener menos errores y fallos que corregir.

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

  • No es necesario compilar para probarlo.

  • El código en ejecución es pequeño.

  • It has duck-typing and polymorphism by nature.

Siendo las principales desventajas:

  • Menor rendimiento que los lenguajes de tipado estático.

  • Mayor dificultad para refactorizar (no se pueden trazar los símbolos).

  • Algunos errores que se detectarían en el tiempo de compilación en lenguajes de tipado estático, solo aparecen cuando se ejecuta el código (porque el análisis gramatical es más estricto).

  • Menos flexibilidad para el auto completado de código (los tipos de algunas variables solo se conocen en tiempo de ejecución).

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 y asignaciones

Todas las variables en lenguajes de tipado dinámico son del tipo "variant". Esto significa que su tipo no es fijo, y solo se puede modificar mediante la asignación. Ejemplo:

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 de funciones:

Las funciones también tienen una naturaleza dinámica, lo que significa que pueden ser invocadas con diferentes argumentos, por ejemplo:

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.

Punteros y referencia:

En lenguajes estáticos como C o C++ (y hasta cierto punto Java y C#), hay una distinción entre una variable y un puntero/referencia a una variable. Este último permite al objeto ser modificado por otras funciones pasando una referencia del original.

En C# o Java, todo lo que no es un tipo integrado (int, float, y algunas veces string) es siempre un puntero o una referencia. Las referencias son recogidas por el recolector de basura automáticamente, lo que significa que se borran cuando no se están usando. Los lenguajes dinámicos tienden a usar este modelo de memoria también. Algunos ejemplos:

  • 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

Los arrays en lenguajes de tipado dinámico pueden contener mezclados muchos tipos diferentes de datos y son siempre dinámicos (pueden cambiar su tamaño en cualquier momento). Compara, por ejemplo, los arrays en lenguajes de tipado estático:

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.

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

En lenguajes de tipado dinámico, los arrays pueden replicar otros tipos de datos, como listas:

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

O conjuntos desordenados:

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

Diccionarios

Los diccionarios son una herramienta poderosa en lenguajes de tipado dinámico. La mayoría de los programadores que provienen de lenguajes de programación estáticos (como C++ o C#) ignoran su existencia y hacen su vida más difícil innecesariamente. Este tipo de datos generalmente no está presente en tales lenguajes (o sólo en forma limitada).

Los diccionarios pueden asignar cualquier valor a cualquier otro valor sin tener en cuenta el tipo de datos utilizado como clave o valor. En contra de la creencia popular, son eficientes porque se pueden implementar con tablas de hash. De hecho, son tan eficientes que algunos idiomas llegan a implementar arrays como diccionarios.

Ejemplo de Diccionario:

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

Los diccionarios también son dinámicos, las claves pueden ser agregadas y eliminadas en cualquier punto a muy bajo costo:

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

Los diccionarios también se pueden utilizar como marcadores de datos o estructuras rápidas. Mientras que los diccionarios GDScript se asemejan a los diccionarios python, también soportan sintaxis e indexación estilo Lua, lo que lo hace útil para escribir estados iniciales y estructuras 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 y 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)

Los tipos de datos de los containers (matrices y diccionarios) son iterables. Los diccionarios permiten iterar entre las claves:

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

Iterar con índices también es posible:

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

La función range() puede tomar 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) {}

Traducir a:

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

Se vuelve:

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

While

Los bucles while() son los mismos en todas partes:

var i = 0

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

Itineradores personalizados

Puedes crear iteradores personalizados en caso de que los predeterminados no cumplan con tus necesidades anulando las funciones _iter_init, _iter_next, e _iter_get de la clase Variant en su script. A continuación se muestra un ejemplo de implementación de un iterador avanzado:

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

Y se puede utilizar como cualquier otro iterador:

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

Asegúrate de restablecer el estado del iterador en _iter_init, de lo contrario, los bucles for que utilizan iteradores personalizados no funcionarán como se espera.

Tipado dinámico (duck typing)

Uno de los conceptos más difíciles de entender a la hora de pasar de un lenguaje de tipado estático a otro dinámico es el duck typing. Duck typing hace que el diseño general del código sea mucho más simple y sencillo de escribir, pero no es obvio cómo funciona.

Por ejemplo, imagina una situación en la que una gran roca cae por un túnel, aplastando todo en su camino. El código para la roca, en un lenguaje de tipado estático sería algo así como:

void BigRollingRock::on_object_hit(Smashable *entity) {

    entity->smash();
}

De esta manera, todo lo que pueda ser aplastado por una roca tendrá que heredar Smashable. Si un personaje, un enemigo, un mueble o una pequeña roca se pudieran destruir, tendrían que heredar de la clase Smashable, lo que posiblemente requeriría una herencia múltiple. Si la herencia múltiple no era deseada, entonces tendrían que heredar una clase común como Entity. Sin embargo, no sería muy elegante añadir un método virtual smash() a Entity sólo si unos pocos de ellos pueden ser aplastados.

Con lenguajes de tipado dinámico, esto no es un problema. Duck typing asegura de que sólo tengas que definir una función smash() donde sea necesario y nada más. No hay necesidad de considerar la herencia, clases base, etc.

func _on_object_hit(object):
    object.smash()

Y eso es todo. Si el objeto que golpea a la roca grande tiene un método smash(), entonces será llamado. No se necesita herencia ni polimorfismo. Los lenguajes de tipado dinámico solo se preocupan de que la instancia tenga los métodos o miembros deseados, no que hereda ni el tipo de clase. La definición de Duck Typing debería hacer esto más claro:

"Cuando veo un ave que camina como un pato, nada como un pato y grazna como un pato, entonces llamo al ave pato"

En este caso, se traduce como:

"Si el objeto puede ser golpeado, no me importa lo que es, solo golpéalo."

Sí, en su lugar deberíamos llamarlo Hulk Typing.

Es posible que el objeto que recibe el impacto no tenga una función smash(). Algunos lenguajes de tipado dinámico simplemente ignoran una llamada a un método cuando no existe, pero GDScript es más estricto, por lo que es deseable comprobar si la función existe:

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

Entonces, simplemente define el método y todo lo que la roca toca puede ser aplastado.