02 Experimentos GDScript - hektorprofe/godot-descubre GitHub Wiki

10. GDScript

Escribir un script simple que imprima "¡Hola Godot!" en la consola.

  • Mediante la programación podemos modificar el valor de las propiedades de los nodos.
  • En Godot cada Nodo permite enlazar un bloque de código con las instrucciones que debe ejecutar.
  • Estos bloques de código se denominan Scripts y Godot permite programarlos en GDScript, C# o mediante la tecnología GDNative con C y C++.
  • En esta serie vamos a utilizar el lenguaje GDScript, se parece mucho a Python y es muy amigable.
  • Podemos crear un Script en el nodo seleccionado presionando el icono del pergamino o desde el inspector.
  • Vamos a añadir un Script a nuestro nodo Main y lo guardaremos en el directorio scripts/Main.gd.
  • Dejaremos marcada la opción Plantilla, eso generará un código inicial de prueba.
  • Al crearlo se abrirá el editor integrado de Scripts con el código de prueba:
extends Node2D

# Called when the node enters the scene tree for the first time.
func _ready() -> void:
    pass # Replace with function body.
  • Si nunca habéis programado esto será muy confuso, pero no os preocupéis. Más que saber de memoria las instrucciones, lo importante es hacerse una idea de lo que el código está haciendo.
  • Arriba del todo la línea extends Node2D indica que el script extiende un Nodo2D, eso significa que desde el código del script tenemos acceso a las propiedades de esa clase de Nodo.
  • A continuación se definen dos conjuntos de código _ready y _process. La palabra delante func nos indica que esos conjuntos son funciones de código y están enlazadas, tal y como se explica en los comentarios encima de ellas.
  • Los comentarios no se interpretan, empiezan con un corchete # y nos permiten realizar anotaciones en el script.
  • Estas dos funciones están enlazadas a dos momentos durante la ejecución del nodo.
  • La función que nos interesa por ahora es _ready, la otra podemos borrarla. Ready, del inglés preparado, se llama cuando el nodo entra al árbol de escenas por primera vez, es decir, se ejecuta una sola vez al cargar la escena actual.
  • Dentro de la función encontramos una instrucción llamada pass, ésta no hace nada y solo se utiliza para indicar que la función está vacía. Es necesaria porque una función debe tener por lo menos una instrucción. Sabemos que está dentro de la función porque está a una tabulación o cuatro espacios respecto al resto del código del script. El editor nos muestra las tabulaciones con una flecha, eso nos ayuda a entenderlo mejor.
  • Vamos a borrar ese pass y a escribir nuestra primera instrucción de código, que como manda la tradición es un saludo.
  • Escribiremos print() y ejecutaremos el programa, veremos que no pasa nada.
  • Esto que hemos escrito es la llamada de una función que nos proporciona Godot identificada con el nombre print.
  • Las funciones son bloques de código con un nombre, sirven para organizar código en su interior y eso las hace reutilizables.
  • En este caso la función print() permite mostrar datos en la terminal de Salida. Como no le hemos indicado ningún dato no se muestra nada. Podemos consultar la documentación de la función print() presionando Control+Clic en ella, pero está en inglés.
  • Si le pasamos un número entre los paréntesis la cosa cambiará print(12345).
  • Un número no dice mucho, mejor intentemos escribir un texto print(¡Hola Godot!).
  • Veréis que no es necesario ejecutar el programa para ver que tenemos un error, la línea es incorrecta.
  • Un texto es un tipo de dato especial con sus propias normas de escritura.
  • Para que Godot sepa que ese mensaje es un texto debemos escribirlo entre comillas dobles o simples, como en Python.
func _ready() -> void:
    print("¡Hola Godot!")
  • Si ejecutamos el programa los cambios en el Script se guardarán automáticamente, también podemos hacer Control+S.
  • Felicidades, habéis creado y ejecutado vuestro primer script de código.
  • En las próximas 7 prácticas os enseñaré los fundamentos de la programación con GDScript.
  • Si nunca habéis programado os sentiréis perdidos, pero tened fe en vuestro yo del futuro y no lo dejéis a medias.
  • Os aseguro que la recompensa por el esfuerzo realizado será enorme, muchos ánimos.

image

11. Literales y Variables Simples

Explicar cómo declarar y usar variables para almacenar valores como números y cadenas.

  • En el anterior experimento nos ha quedado claro que al programar no es lo mismo referirnos a un número que a un texto.
  • Mientras que un número se puede escribir directamente, una cadena de texto requiere delimitarla entre comillas simples o dobles.
  • Si sois nuevos en el mundo de la programación, tenéis que grabaros a fuego que la utilidad de programar es manipular información.
  • Una información que se representa en forma de datos de diferentes tipos, como por ejemplo los números y las cadenas de texto.
  • Cuando llamamos a la función print("¡Hola Godot!") con una cadena de texto, estamos pasando un dato con un valor literal.
  • Los datos literales se almacenan temporalmente en algún lugar desconocido de la memoria del sistema hasta que se utilizan.
  • Sin embargo, a veces queremos saber donde se encuentra ese dato para poder reutilizarlo y ahí es donde aparecen las variables.
  • Una variable es la consecuencia de almacenar un dato en un lugar de la memoria identificado con un nombre.
  • Siempre que sepamos el nombre de la variable podemos recuperar el valor del dato que hemos guardado en su interior.
  • En Godot podemos definir una variable con la palabra clave var de la siguiente forma:
func _ready() -> void:
    var mensaje = "¡Hola Godot!"
  • Al asignar mediante el operador igual un dato a la variable mensaje, podemos hacer uso de su valor donde lo necesitamos.
func _ready() -> void:
    var mensaje = "¡Hola Godot!"
    print(mensaje)
    print(mensaje)
    print(mensaje)
  • Si en lugar de definir la variable dentro de una función como _ready la definimos antes de este bloque, exportándola con la sintaxis @export, podemos exponerla como una propiedad del Nodo que tiene asignado el script y editarla visualmente, además podemos acceder a ella desde cualquier función definida después:
@export var mensaje = "¡Hola Godot!"

func _ready() -> void:
    print(mensaje)
  • Utilizar variables con diferentes datos nos permite organizar la información de forma lógica:
@export var nombre = "¡Hola Godot!"
@export var nacimiento = 1989

func _ready() -> void:
    print(nombre, nacimiento)  # Héctor1989
  • Una variante de la función print() llamada prints() añade un espacio space entre los valores:
@export var nombre = "¡Hola Godot!"
@export var nacimiento = 1989

func _ready() -> void:
    prints(nombre, nacimiento)  # Hector 1989
  • Esta función nos permite encadenar variables y literales:
func _ready() -> void:
    prints("Me llamo", nombre, "y nací en", nacimiento)  # Me llamo Héctor y nací en 1989
  • De forma intuitiva podemos utilizar operadores matemáticos para operar números. Por ejemplo:
func _ready() -> void:
    prints("Me llamo", nombre, "y nací en", nacimiento)  # Me llamo Héctor y nací en 1989
    prints("Tengo o voy a cumplir", edad, "años")  # Tengo o voy a cumplir 35 años
  • Incluso podemos exportar la variable edad y comprobar como en el inspector remoto se hace el cálculo:
@export var edad = 0

func _ready() -> void:
    prints("Me llamo", nombre, "y nací en", nacimiento)  # Me llamo Héctor y nací en 1989
    edad = 2024 - nacimiento  # Comprobar el inspector remoto después de ejecutar el programa
    prints("Tengo o voy a cumplir", edad, "años")  # Tengo o voy a cumplir 35 años
  • La función _ready() solo se ejecuta una vez al poner en marcha el programa, pero si realizamos el cálculo en una función como _process que se llama constantemente, en cada fotograma del juego, al cambiar el año de nacimiento la edad se recalculo automáticamente:
func _process(delta) -> void:
    edad = 2024 - nacimiento  # no se pueden exportar cálculos
  • Hablaremos sobre la función _process() más adelante, por ahora espero que haya quedado clara la diferencia entre un valor literal y una variable.
  • Aunque ambos representan información, los valores literales no se pueden reutilizar pero las variables sí.
  • Aprenderemos mucho más sobre la marcha así que por ahora dejémoslo aquí.

12. Funciones Sencillas

Enseñar a definir y llamar funciones para ejecutar bloques de código.

  • ¿Recordáis cuando hablamos de que las funciones son bloques de código reutilizable?
  • Godot nos proporciona dos tipos de funciones, las de uso general y las especiales.
  • Las funciones print() y prints() son funciones de uso general disponibles en cualquier lugar y momento.
  • Mientras que las funciones como _ready son especiales y están ligadas a momentos de ejecución del programa.
  • Estas funciones especiales se llaman automáticamente y lo que hacemos al programarlas en un script es sobrescribirlas.
  • Al sobrescribir una función especial podemos ejecutar nuestro propio código en un momento especifico del ciclo de juego.
  • Pero quizá haya momentos en que necesitemos una función más personalizada que Godot no nos proporciona.
  • Es entonces cuando podemos optar por programar nuestras propias funciones de código reutilizable.
  • Para programar una función lo haremos en el bloque principal siguiendo el ejemplo de _ready:
func saludar() -> void:
    print("¡Hola Godot!")

func _ready() -> void:    
    # ...
  • La palabra void hace referencia a un tipo de dato vacío y al indicarla con la flecha después del nombre de la función significa que la función no retorna ningún valor explícito. Aunque no es obligatorio ponerlo, ayuda a entender de un vistazo si se retorna algo o no.
  • Una vez tenemos definida nuestra función tenemos que llamarla, hacerlo es tan sencillo como ejecutar su nombre con los paréntesis:
func saludar() -> void:
    print("¡Hola Godot!")

func _ready() -> void:    
    saludar()   
    saludar()   
    saludar()
  • Una función puede recibir y retornar datos, veamos como podemos recibir información para usarla en su interior de forma dinámica.
  • Si durante la definición indicamos el nombre de una o más variables, es como si definiéramos esas variables dentro de la función.
  • Estas variables se denominan parámetros de la función y permiten capturar los datos por el orden que se le envían:
func saludar(nombre) -> void:
    print("¡Hola ", nombre, ", muchos ánimos aprendiendo Godot!")

func _ready() -> void:    
    saludar("Ana")   
    saludar("Marcos")   
  • Una misma función con distintos valores de entrada generando diferentes resultados, es es la clave del código dinámico y la reutilización.
  • Hagamos otro ejemplo muy sencillo para ilustrar como se retorna un valor desde una función.
  • Supongamos que necesitamos programar una función llamada sumar que recibe dos números y debe mostrar el resultado:
func sumar(a, b) -> void:    
    print(a+b)

func _ready() -> void:    
    sumar(5, 10)  # 15
  • Esta función es correcta, el problema es que no nos da mucho juego, porque imaginad que queremos sumar el resultado de la primera llamada 10 y 5 a otro número. ¿Cómo podemos hacerlo?
  • Para conseguir esto podemos retornar el valor resultante en lugar de mostrarlo por pantalla con un print(), fijaros:
func sumar(a, b) -> void:    
    return a+b
  • Al retornar un valor podemos indicar de qué tipo es. En este caso se trata de un número entero, por lo que pondremos int. Si fuera un número decimal pondriamos float y para una cadena de texto el identificador sería String:
func sumar(a, b) -> int:    
    return a+b

func _ready() -> void:       
    sumar(10, 5)  # 15
  • Al ejecutar este código no se muestra ninguna salida, podemos pasar las llamadas directamente a un print() para que interprete el resultado de la llamada como un número:
func _ready() -> void:       
    print(sumar(10, 5))  # 15
  • Como el resultado devuelto se trata como un literal entero podemos operarlo junto otro número o incluso el resultado de otra suma:
func _ready() -> void:       
    var resultado_1 = sumar(10, 5)  # 15
    var resultado_2 = sumar(resultado_1, -3)  # 12 
    print(resultado_2)  # 12

    # Forma directa
    print(sumar(sumar(10, 5), -3))  # 12
  • Aprovechando que esto es un juego que se ejecuta constantemente, si exportamos las variables podemos calcular la suma dinámicamente calculándola en la función _process. Y ya que estamos, una buena práctica es establecer el tipo de dato de cada variable:
@export var numero_1 : int = 10
@export var numero_2 : int = 5
@export var resultado : int  = 0

func _process(delta) -> void:
    resultado = sumar(numero_1, numero_2)
  • Os reto a programar una calculadora con otras tres funciones llamadas restar, multiplicar y dividir para hacer lo propio con dos números decimales de tipo float. Tenéis que exportar los resultados de cada operación en 4 variables llamadas suma, resta, multiplicacion y division. Os dejo el programa base, pausad el vídeo, intendadlo y luego os enseño la solución:
# Trabajaremos todo con decimales
@export var numero_1 : float = 10.0
@export var numero_2 : float = 5.0

@export var suma : float = 0
@export var resta : float = 0
@export var multiplicacion : float = 0
@export var division : float = 0

func sumar(a, b) -> int:    
    return a+b

# Completa el programa a partir de aquí

func restar(?) -> ?: 
    return ?

func multiplicar(?) -> ?: 
    return ?

func dividir(?) -> ?:  
    return ?

func _process(delta) -> void:
    suma = sumar(numero_1, numero_2)
    resta = ?
    multiplicacion = ?
    division = ?
  • Aquí va la solución:
@export var numero_1 : float = 10.0
@export var numero_2 : float = 5.0

@export var suma : float = 0
@export var resta : float = 0
@export var multiplicacion : float = 0
@export var division : float = 0

func sumar(a, b) -> float:    
    return a+b

func restar(a, b) -> float:    
    return a-b

func multiplicar(a, b) -> float:    
    return a*b

func dividir(a, b) -> float:    
    return a/b

func _process(delta) -> void:
    suma = sumar(numero_1, numero_2)
    resta = restar(numero_1, numero_2)
    multiplicacion = multiplicar(numero_1, numero_2)
    division = dividir(numero_1, numero_2)

13. Condición If/Else Básica

Mostrar cómo usar condiciones para ejecutar diferentes bloques de código.

  • En la programación las instrucciones se ejecutan siempre de forma ordenada y sucesiva, eso se denomina flujo del programa.
  • Sin embargo también es posible programar nuestro código para que éste tome decisiones y realice diferentes acciones.
  • Este concepto se conoce como el control de flujo y se puede implementar con dos tipos de instrucciones: condiciones y bucles.
  • Centrémonos en las primeras, las condiciones, que permiten ejecutar un bloque de código dependiendo de un valor.
  • Como las condiciones se basan en determinar un valor, es imprescindible saber comparar valores, así que empecemos por ahí.
  • Los tipos de datos más elementales como los números enteros int, los decimales float y los textos String se conocen como tipos primitivos.
  • Pero a parte de estos tres hay uno más que aún no conocemos, se trata de los booleanos o lógicos, con código bool.
  • El tipo booleano representa la forma más primaria de transmitir información y tiene solo dos posibles valores lógicos: verdadero true y falso false
@export var logico : bool = false
  • El tipo booleano es muy especial, no solo es que tenga dos únicos valores, es que uno es el contrario del otro.
  • Según la negación lógica, cuando algo es verdadero no es falso y cuando algo no es falso es verdadero.
  • En programación existe el operador de negación ! o not, con él podemos negar de valor un booleano:
extends Node2D

@export var logico : bool = false
@export var logico_negado : bool = false

func _process(_delta) -> void:       
    logico_negado = not logico  # !logico
  • Lo interesante es que las operaciones comparativas siempre devuelven un booleano true o false.
  • Las dos operaciones comparativas básicas permiten saber si dos valores son iguales == o distintos !=:
func _ready(_delta) -> void:    
    print(true == false)       # false
    print(true != false)       # true
    print("Godot" == "Godot")  # true
    print("Godot" == "godot")  # false
    print("Godot" != "godot")  # true
    print(7 == 3)              # false
    print(7 != 3)              # true
  • Los datos numéricos enteros y decimales permiten también las comparaciones mayor >, menor <, mayor o igual >= y menor o igual <=:
func _ready() -> void:   
    print(7 > 3)               # true
    print(7 >= 7)              # true
    print(7 < 3)               # false
    print(7 <= 3)              # false
  • El quid de la cuestión es utilizar el resultado de una comparación como fuente para condicionar el código.
  • Mediante la instrucción if podemos crear un bloque de código condicionando a una expresión booleana verdadera true:
@export var condicion : bool
@export var resultado : String

func _process(_delta) -> void:   
    if condicion: # condicion == true
        resultado = "Condición es true"
    if !condicion: # condicion != true, condicion == false
        resultado = "Condición es false"
  • Vamos a crear una función para comparar dos palabra usando condiciones:
func comparar_palabras(palabra1, palabra2) -> void:
    if palabra1 == palabra2:
        prints(palabra1, palabra2, "-> Es la misma palabra")
    if palabra1 != palabra2:
        prints(palabra1, palabra2, "-> Son palabras distintas")

func _ready() -> void:   
    comparar_palabras("Godot", "Godot")
    comparar_palabras("Godot", "godot")
  • El bloque que contiene las instrucciones del if tiene una contraparte llamada instrucción else que si la encadenamos al if permite ejecutar un código cuando la expresión evaluada es false. Esto nos ahorrará escribir dos if distintos:
func _process(_delta) -> void:   
    if condicion: # condicion == true
        resultado = "Condición es true"
    else: # condicion != true, condicion == false, else
        resultado = "Condición es false"
    
func comparar_palabras(palabra1, palabra2) -> void:
    if palabra1 == palabra2:
        prints(palabra1, palabra2, "-> Es la misma palabra")
    else:
        prints(palabra1, palabra2, "-> Son palabras distintas")

func _ready() -> void:   
    comparar_palabras("Godot", "Godot")
    comparar_palabras("Godot", "godot")
  • Por último debéis saber que existe la posibilidad de encadenar una o múltiples instrucciones elif entre un if y un else para comprobar otros caso. Para ilustrarlo vamos a crear una función llamada comparar_numeros que te diga si un número es mayor, menor o igual que otro:
func comparar_numeros(numero1, numero2) -> void:
    if numero1 > numero2:
        prints(numero1, "es mayor que", numero2)
    elif numero1 < numero2: 
        prints(numero1, "es menor que", numero2)
    elif numero1 == numero2: 
       prints(numero1, "es igual que", numero2)

func _ready() -> void:   
    comparar_numeros(100, 99)
    comparar_numeros(55, 72)
    comparar_numeros(38, 38)
  • Cuando sabemos que no hay más posibilidades, siempre podemos programar un else al final como caso por defecto y funcionará igual:
func comparar_numeros(numero1, numero2) -> void:
    if numero1 > numero2:
        prints(numero1, "es mayor que", numero2)
    elif numero1 < numero2: 
        prints(numero1, "es menor que", numero2)
    else:
        prints(numero1, "es igual que", numero2)
  • Cuando usamos múltiples if se comprueban todas las posibilidades, al encadenar if-elif-else cuando se entra a un bloque los otros se descartan, siendo por tanto una forma de optimizar la ejecución del código.

  • En esta lección hemos aprendido cómo las condiciones nos permiten llevar el código por diferentes caminos.

14. Match para Controlar el Flujo

Mostrar cómo match puede usarse como una estructura de control de flujo.

  • Las condiciones if-elif-else son perfectas para la mayoría de casos, especialmente cuando implican variables.
  • Ahora bien, existe otra forma de condicionar el código cuando queremos comprobar muchos casos con valores literales.
  • En el desarrollo de videojuegos el ejemplo más ilustrativo es lo que se conoce como una máquina de estados.
  • Supongamos que necesitamos gestionar los diferentes estados de un personaje que podemos codificar mediante cadenas de texto. Podemos utilizar unos bloques if-elif-else para ejecutar el código pertinente en cada caso:
var estado = "parado"

if estado == "parado":
   print("El personaje está parado")
elif estado == "caminar":
   print("El personaje está caminando")
elif estado == "correr":
   print("El personaje está corriendo")
elif estado == "atacar":
   print("El personaje está atacando")
else:
   print("El personaje está haciendo otra cosa")
  • Este código es funcional pero tenemos que escribir constantemente if estado == y lo que sea.
  • Godot incluye una instrucción llamada match para ejecutar casos específicos. La sintaxis de uso es la siguiente:
var estado = "parado"

match estado:
   "parado":
       print("El personaje está parado")
   "caminar":
       print("El personaje está caminando")
   "correr":
       print("El personaje está corriendo")
   "atacar":
       print("El personaje está atacando")
   _:
       print("El personaje está haciendo otra cosa")
  • Es una instrucción bastante sencilla pero hay que prestar atención a la correcta tabulación de cada bloque anidado.
  • También fijaros que el caso por defecto se escribe con un símbolo _, lo que equivaldría al else de una condición if.

15. Recursos de Datos

Enseñar a usar recursos para almacenar conjuntos de información.

  • Con lo visto podemos crear Scripts para jugar con las propiedades de los Nodos, pero antes quiero enseñaros otro concepto importante.
  • Hemos aprendido que las variables nos permiten almacenar un único dato en su interior, por ejemplos un número, una cadena de texto o un booleano.
  • Pero, ¿y si necesitamos manejar un conjunto de datos? ¿Tenemos que crear una variable para cada uno? Pues no, para eso existen las estructuras.
  • Hay dos estructuras clásicas: los diccionarios y los arreglos, sin embargo Godot tiene una versión mejorada de los diccionarios llamada recursos.
  • Un recurso es un nuevo tipo de dato que podemos definir para almacenar los datos que nosotros queramos, son como plantillas de información.
  • La mejor forma de ver esto es con un ejemplo práctica así que vamos a por ello.
  • Supongamos que necesitamos almacenar los datos de un personaje para un videojuego RPG.
  • Este personaje tiene diferentes campos con tipos diferentes, por ejemplo:
@export var nombre : String = "Félix"
@export var profesion : String = "Erudito"
@export var vida : int = 150
@export var mana : int = 300
@export var desbloqueado : bool = true
  • ¿Véis por donde van los tiros? Un personaje como Félix es un conjunto de datos estructurados formado por los campos nombre, profesión, vida, mana y desbloqueado.
  • Y no sólo podemos tener a Félix, la idea es que podamos tener tantos personajes como queramos, cada uno con su propio nombre, profesión, puntos de vida, etc.
  • Pues en Godot un recurso es como una plantilla para definir un nuevo tipo de dato, en nuestro caso lo podemos llamar Personaje y contendrá los campos nombre, profesión, etc.
  • Para crear un nuevo recurso vamos a crear un script en el proyecto, extenderemos el tipo Resource y le daremos un nombre class_name seguido de las variables que forman un personaje:
# Personaje.gd
extends Resource

class_name Personaje

@export var nombre : String
@export var profesion : String
@export var vida : int
@export var mana : int
@export var desbloqueado : bool
  • Listo, ya tenemos la plantilla. Ahora podemos exportar una nueva variable de este tipo Personaje para crearla en el propio inspector de forma cómoda:
extends Node2D

@export var felix : Personaje  # Datos inspector: "Félix", "Erudito", 150, 300, true

func _ready() -> void:
    pass
  • Este recurso creado desde el inspector solo existe localmente en la memoria de la escena, pero una vez establecidos sus parámetros tenemos la opción de Guardarlo en el disco, haciendo clic derecho Guardar como y en un directorio resources/Felix.tres. Desde este momento los datos de Felix están en el disco duro, podemos acceder a su información en cualquier momento y modificarla, incluso podedmos acceder a sus datos desde cualquier script.

  • Esta es una forma excelente y fácil de organizar nuestros personajes. ¡Vamos a crear más! En el directorio resources, Clic derecho > Crear recurso > Personaje, y creamos varios:

Arturo.tres = {'nombre': "Arturo", 'profesion': "Caballero", 'vida': 350, 'mana': 150, 'desbloqueado': false}, 
Merlin.tres = {'nombre': "Merlín", 'profesion': "Mago",      'vida': 100,  'mana': 500, 'desbloqueado': false}, 
Robin.tres  = {'nombre': "Robin",  'profesion': "Arquero",   'vida': 200, 'mana': 100, 'desbloqueado': true}, 
  • Una vez tenemos 4 personajes, cada uno en su fichero bien organizado, vamos a cargarlos todos en nuestro script automáticamente, para ello usaremos la función preload, no hace falta escribirla, solo debemos seleccionar el recurso y arrastrarlo al editor de código, justo en el momento de soltarlo presionamos Control y autocompletará esa parte:
extends Node2D

@export var felix : Personaje = preload("res://resources/Felix.tres")
@export var arturo : Personaje = preload("res://resources/Arturo.tres")
@export var merlin : Personaje = preload("res://resources/Merlin.tres")
@export var robin : Personaje = preload("res://resources/Robin.tres")

func _ready() -> void:
    pass
  • ¿Y ahora qué podemos hacer con estos personajes? Pues por ejemplo podemos crear una función que muestre su información, accediendo a sus datos usando un puntito:
func resumen(personaje: Personaje) -> void:
    print("------------------")
    prints("Nombre:", personaje.nombre)
    prints("Profesion:", personaje.profesion)
    prints("Vida:", personaje.vida)
    prints("Maná:", personaje.mana)
    prints("Desbloqueado:", personaje.desbloqueado)

func _ready() -> void:
    resumen(felix)
    resumen(arturo)
    resumen(merlin)
    resumen(robin)
  • ¿Genial verdad? Pero esto no es todo porque quizá os podáis plantear la duda de si es necesario crear la función resumen en cada scripts donde necesitemos consultar los datos de nuestros personajes. ¡Y la respuesta es que no es necesario! Porque de hecho los propios recursos pueden incluir funciones. Solo tenemos que modificar la plantilla Personaje y agregar nuestra función resumen usando los propios datos del recurso en lugar de pasarle un personaje:
extends Resource

class_name Personaje

@export var nombre : String
@export var profesion : String
@export var vida : int
@export var mana : int
@export var desbloqueado : bool

func resumen() -> void:
    print("------------------")
    prints("Nombre:", nombre)
    prints("Profesion:", profesion)
    prints("Vida:", vida)
    prints("Maná:", mana)
    prints("Desbloqueado:", desbloqueado)
  • Y ahora se viene lo bueno, allá donde tengamos un personaje solo tenemos que llamar su función con un puntito:
extends Node2D

@export var felix : Personaje = preload("res://resources/Felix.tres")
@export var arturo : Personaje =  preload("res://resources/Arturo.tres")
@export var merlin : Personaje =  preload("res://resources/Merlin.tres")
@export var robin : Personaje =  preload("res://resources/Robin.tres")

func _ready() -> void:
    felix.resumen()
    arturo.resumen()
    merlin.resumen()
    robin.resumen()
  • ¿No es esto mega práctico y conveniente? Los recursos no solo permiten crear estructuras de datos sino programar sus propias funcionalidades. Es la forma definitiva de estructurar los datos en Godot.

16. Arrays de Datos

Explicar cómo crear y manipular arrays para almacenar múltiples valores.

  • Otra forma de manejar conjuntos de información son los arrays, en español arreglos.
  • Un arreglo es una sucesión de valores definidos entre corchetes y separados con comas. Por ejemplo un array de números:
@export var numeros : Array[float] = [31, 5, 6.75, 8.99, 12, 5]

func _ready() -> void:
    print(numeros)
  • Los arreglos se consideran estructuras ordenadas porque sus valores conservan el orden en que se definen.
  • Es posible acceder al valor de una posición del arreglo usando un índice numérico entre corchetes.
  • El valor del índice para la primera posición siempre empieza valiendo cero, el segundo uno y así:
func _ready() -> void:
    print(numeros[0])  # 31
    print(numeros[1])  # 6.75
    print(numeros[2])  # 8.99
  • Para acceder al valor almacenado al final del arreglo podemos usar el índice -1:
func _ready() -> void: 
    print(numeros[-1])  # 5
  • Podemos modificar el valor de una posición asignando uno nuevo también usando el índice:
func _ready() -> void: 
    numeros[0] = 9999
    numeros[3] = 7777
    print(numeros)
  • ¿Recordáis la función resumen() que programamos dentro del recurso Personaje? Los arreglos tienen un montón de funciones como esa para realizar mil cosas. Para diferenciarlas de las funciones normales, las que forman parte de una clase de dato reciben el nombre de métodos de clase:
func _ready() -> void: 
    # Métodos de la clase de dato Array
    numeros.append(100)     # para añadir un valor al final
    print(numeros)

    print(numeros.size())   # tamaño
    print(numeros.has(500)) # comprobar
    print(numeros.count(5)) # contar
    print(numeros.min())    # valor mínimo
    print(numeros.max())    # valor máximo
  • Sea como sea, la magia de los arreglos aparece al utilizarlos en conjunto con variables que substituyen el índice, eso añade dinamismo al código:
@export var indice : int
@export var valor : float

var numeros = [31, 6.75, 8.99, 12, 5]

func _process(delta) -> void: 
    valor = numeros[indice]
  • Aumentando o decrementando el índice desde el inspector podemos recorrer los valores del array, pero hay que ir con cuidado, si el índice está fuera del arreglo el programa se detendrá con un fallo. Para solucionar este problema debemos asegurarnos que el índice siempre sea mayor o igual que cero y menor o igual a la longitud del array:
@export var indice : int
@export var valor : float

@export var numeros : Array[float] = [31, 6.75, 8.99, 12, 5]

func _process(delta) -> void: 
    if indice < 0: 
        indice = 0
    if indice > numeros.size() - 1: 
        indice = numeros.size() - 1 # recortamos por el final
    valor = numeros[indice]
  • Existe una función que sirve para recortar un valor entre un mínimo y un máximo, se llama clamp y podemos usarla para hacer lo mismo en una sola línea:
@export var indice : int
@export var valor : float

@export var numeros : Array[float] = [31, 6.75, 8.99, 12, 5]

func _process(delta) -> void: 
    indice = clamp(indice, 0, numeros.size() - 1)
    valor = numeros[indice]
  • Como véis los arreglos son estructuras muy útiles para almacenar datos y no necesariamente deben ser todos del mismo tipo, podemos mezclar números, cadenas, booleanos e incluso podemos tener arrays de recursos. Vamos a crear un arreglo de pesonajes para poder manejarlos de forma muy cómoda, podemos definirlos desde el inspector o con la técnica de arrastrar y presionar Control:
@export var personajes : Array[Personaje] = [
    preload("res://resources/Felix.tres"),
    preload("res://resources/Arturo.tres"),
    preload("res://resources/Merlin.tres"),
    preload("res://resources/Robin.tres")
]
  • Como dato a tener en cuenta, dado que el conjunto que forma un array y su índice equivale al propio valor contenido, es posible ejecutar el método resumen() directamente:
func _ready() -> void: 
   personajes[1].resumen()
   personajes[-1].resumen()
  • Igual que antes, jugando con un el índice podemos recorrer dinámicamente todos los personajes del arreglo (recordad inspeccionar la escena remota):
@export var indice : int
@export var personaje : Personaje

func _process(delta) -> void: 
    indice = clamp(indice, 0, personajes.size() - 1)
    personaje = personajes[indice]
  • ¿Qué os parecen los arreglos? ¿Bastante útiles no?

17. Bucles For Simples

Introducir bucles para ejecutar un bloque de código múltiples veces.

  • Esta es la última lección dedicada a los fundamentos de GDScript, a partir de la siguiente utilizaremos Nodos para crear experimentos interactivos.
  • Rememorando os expliqué que existen dos conceptos elementales relacionados con el control de flujo: las condiciones y los bucles.
  • Hemos aprendido a utilizar condiciones if-elif-else y la sentencia match para dividir e flujo del código, ahora tocan los bucles.
  • Los bucles son bloques de instrucciones que se ejecutan repetidamente. Existe más de una forma de programarlos pero nosotros vamos a centrarnos solo en los bucles for, en español conocido como bucles para.
  • En GDScript el bucle for se utiliza a partir de una estructura de datos como por ejemplo un arreglo, permitiéndonos repetir un codigo para cada valor almacenado en la estructura. Por ejemplo, para un arreglo con 5 elementos el bucle for repetirá un código 5 veces.
  • La sintaxis es muy sencilla porque la propia palabra clave for (para) nos explica el funcionamiento:
# Para cada valor en el arreglo repetir el bloque de código
@export var palabras : Array[String] = ["Hola", "como", "estás", "hoy"]

func _ready() -> void: 
    for palabra in palabras:
        print(palabra)  
  • Como he explicado, el for siempre va ligado a una estructura que determina sus repeticiones.
  • Es por esa razón que incluso se necesitamos repetir un código ajeno a una estructura necesitamos tener una.
  • Para ayudarnos en ese contexto tenemos una función llamada range() que permite generar al vuelo un arreglo de longitud determinada:
print(range(10))  # generar array con 10 valores de 0 a 9
  • Podemos utilizar este array de números para repetir un bloque tantas veces como queramos:
func _ready() -> void: 
    # Repetir un código 5 veces
    for i in range(5):
        prints("Repeticion del bloque: ", i+1, "veces") 
  • Por convención suele utilizarse el nombre de variable i porque es corto y está vinculado a la palabras índice o iterador.
  • Bien, ya sabemos como repetir un código multiples veces, pero ¿y si necesitamos modificar todos los valores de un arreglo?
  • Sabemos que para modificar un valor en un arreglo necesitamos hacerlo a través del índice, lo siguiente no funciona:
func _ready() -> void: 
    var numeros = [50, 125, 150, 100]

    for numero in numeros:
        numero = numero * 100

    print(numeros)  # El arreglo no cambia
  • La razón por la que no funciona es que numero es una variable que existe como una copia del valor que se va recorriendo.
  • Para modificar los valores necesitamos referirnos mediante un índice que haga referencia a cada posición del elemento iterado.
  • Es decir, en lugar de recorrer el bucle valor a valor, lo tenemos que recorrer índice a índice y hay una forma perfecta de conseguirlo.
  • ¿Recordáis un método de los arreglos llamado size() que devuelve el número de elementos en un arreglo?
  • Utilizando la longitud que devuelve size() como parámetro de la función range() podemos crear un arreglo de la misma longitud que el que queremos modificar pero que contiene índices:
func _ready() -> void: 
    var numeros = [50, 125, 150, 100]

    for i in range(numeros.size()):
        prints("La posición", i, "almacena el valor", numeros[i])
  • Con esto ya tenemos todo lo necesario para modificar los valores de forma secuencial:
func _ready() -> void: 
    var numeros = [50, 125, 150, 100]

    for i in range(numeros.size()):
        numeros[i] *= 100

    print(numeros)  # El arreglo ha cambiado
  • Pero no siempre necesitamos utilizar índices para modificar los valores de un arreglo. Y esto puede sonar confuso después de tomarnos el tiempo para aprender a hacerlo.
  • En la programación los datos se dividen en dos categorías, datos primitivos y datos compuestos. Los datos primitivos son aquellos que se representan así mismos con su propio valores, por ejemplo un número, una cadena de texto o un valor booleano. El propio dato se representa a sí mismo y el tamaño que ocupan en la memoria siempre es constante. Por otro lado los datos compuestos, como su nombre indica, están compuestos de múltiples valores y no se representan por si mismos sino por los valores que contienen, por ejemplo un arreglo o un recurso.
  • El quid de la cuestión es que los tipos primitivos se manejan a partir de su valor, mientras que los tipos compuestos lo hacen a partir de su referencia en la memoria. No es necesario que entendáis esto ahora mismo pero necesitaba hacer esa distinción porque es gracias a esa diferencia que podemos hacer lo siguiente:
@export var personajes : Array[Personaje] = [
    preload("res://resources/Felix.tres"),
    preload("res://resources/Arturo.tres"),
    preload("res://resources/Merlin.tres"),
    preload("res://resources/Robin.tres")
]

func _ready() -> void: 
    for personaje in personajes:
        # Bloqueamos todos los personajes de golpe
        personaje.desbloqueado = false
        personaje.resumen()

  • Como véis podemos modificar un campo de todos los personajes sin utilizar un índice, esa es la característica de los tipos referenciados:
func _ready() -> void: 
    for personaje in personajes:
        personaje.resumen()
  • Y pese a todo podemos utilizar igualmente índices para recorrer los arreglos de tipos compuestos:
func _ready() -> void: 
    for i in range(personajes.size()):
        personajes[i].desbloqueado = false
        personajes[i].resumen()
  • Os aconsejo que ante la duda siempre uséis índices al modificar los valores de un arreglo independientemente del tipo de dato.
  • En cualquier caso la programación es algo que se aprende a base de experimentar así que no os sintáis abrumados.
  • A lo largo de la serie tendréis la oportunidad de escribir mucho código y visualizarlo para ir entendiendo los conceptos.
  • Lo importante es que habéis llegado hasta aquí y os merecéis una felicitación por vuestra constancia.
  • A partir de ahora la cosa se pone verdaderamente divertida así que sigamos aprendiendo juntos.