← Volver al listado de tecnologías

Capítulo 2: GDScript - Fundamentos del Lenguaje

Por: Artiko
godotgdscriptprogramacióntutorialfundamentospooseñales

Capítulo 2: GDScript - Fundamentos del Lenguaje

GDScript es el lenguaje nativo de Godot, diseñado específicamente para el desarrollo de videojuegos. Con una sintaxis similar a Python pero optimizado para game development, GDScript es fácil de aprender pero lo suficientemente poderoso para juegos complejos. En este capítulo dominarás todos los aspectos fundamentales del lenguaje.

¿Qué es GDScript?

GDScript es un lenguaje de programación de alto nivel, dinámicamente tipado (con soporte opcional para tipos estáticos), que se integra perfectamente con el motor Godot.

Características Principales

¿Por Qué GDScript y No C# o C++?

AspectoGDScriptC#C++
Curva de aprendizaje📈 Suave📈 Media📈 Empinada
Integración con Godot⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Performance⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Velocidad de desarrollo⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Hot reload✅ Sí⚠️ Limitado❌ No
Documentación⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

Para el 90% de los juegos, GDScript es más que suficiente. Solo considera C# o C++ si necesitas:

Tu Primer Script en GDScript

Estructura Básica

Todo script en GDScript comienza extendiendo una clase base:

# Esto es un comentario
extends Node  # El script hereda de la clase Node

# Variables de clase (propiedades)
var health = 100
var player_name = "Hero"

# Función llamada cuando el nodo entra en la escena
func _ready():
    print("¡Hola desde GDScript!")
    print("Jugador: ", player_name)
    print("Vida: ", health)

# Función llamada cada frame
func _process(delta):
    # delta es el tiempo transcurrido desde el último frame
    pass  # pass significa "no hacer nada"

Funciones del Ciclo de Vida

Godot llama automáticamente estas funciones en momentos específicos:

extends Node

# Se llama una vez cuando el nodo entra en el árbol de escena
func _ready():
    print("Nodo listo")

# Se llama cuando el nodo está a punto de ser destruido
func _exit_tree():
    print("Adiós mundo")

# Se llama cada frame (60 veces por segundo normalmente)
func _process(delta):
    # Para lógica general, UI, input no físico
    pass

# Se llama a intervalos fijos (para física)
func _physics_process(delta):
    # Para movimiento, colisiones, física
    pass

# Se llama cuando hay un evento de input
func _input(event):
    if event is InputEventKey:
        print("Tecla presionada")

# Se llama cuando hay input no manejado
func _unhandled_input(event):
    # Input que no fue consumido por la UI
    pass

Variables y Tipos de Datos

Variables Básicas

GDScript es dinámicamente tipado, pero soporta type hints opcionales:

# Sin tipo (dinámico)
var mi_variable = 10

# Con tipo explícito
var vida: int = 100
var nombre: String = "Jugador"
var velocidad: float = 5.5
var esta_vivo: bool = true

# Constantes (no pueden cambiar)
const MAX_VIDA = 100
const GRAVEDAD = 9.8
const NOMBRE_JUEGO = "Mi Súper Juego"

# Variables con tipo inferido
var puntos := 0  # El := infiere que es int
var mensaje := "Hola"  # Infiere String

Tipos de Datos Primitivos

# Números enteros
var entero: int = 42
var hexadecimal = 0xFF  # 255 en decimal
var binario = 0b1010  # 10 en decimal

# Números decimales
var decimal: float = 3.14
var cientifico = 1.5e3  # 1500.0

# Booleanos
var verdadero: bool = true
var falso: bool = false

# Strings (cadenas de texto)
var texto: String = "Hola Mundo"
var multilinea = """
    Este es un texto
    de múltiples
    líneas
"""
var interpolacion = "Tu vida es: %d" % vida
var concatenacion = "Hola " + "Mundo"

# Null (ausencia de valor)
var nada = null

Tipos de Datos Compuestos

Arrays (Listas)

# Array dinámico
var inventario = []
var numeros = [1, 2, 3, 4, 5]
var mixto = [1, "dos", 3.0, true]

# Array tipado
var enemigos: Array[Node2D] = []
var puntuaciones: Array[int] = [100, 200, 300]

# Operaciones con arrays
inventario.append("Espada")  # Añadir al final
inventario.push_front("Escudo")  # Añadir al inicio
inventario.insert(1, "Poción")  # Insertar en posición
var item = inventario.pop_back()  # Quitar y retornar último
inventario.remove_at(0)  # Eliminar en posición
inventario.clear()  # Vaciar array

# Acceso a elementos
var primero = numeros[0]  # Índice 0
var ultimo = numeros[-1]  # Índice negativo cuenta desde el final
var sublista = numeros.slice(1, 3)  # [2, 3]

# Iteración
for item in inventario:
    print(item)

for i in range(inventario.size()):
    print(i, ": ", inventario[i])

Dictionaries (Mapas/Objetos)

# Dictionary básico
var jugador = {
    "nombre": "Hero",
    "nivel": 1,
    "vida": 100,
    "mana": 50
}

# Dictionary tipado
var stats: Dictionary = {}

# Acceso y modificación
jugador["nivel"] = 2  # Notación de corchetes
jugador.experiencia = 0  # Notación de punto (si la key es válida)

# Añadir nuevas keys
jugador["clase"] = "Guerrero"

# Verificar existencia
if "mana" in jugador:
    print("El jugador tiene mana")

if jugador.has("vida"):
    print("Vida: ", jugador.vida)

# Obtener con valor por defecto
var defensa = jugador.get("defensa", 0)  # Retorna 0 si no existe

# Iteración
for key in jugador:
    print(key, ": ", jugador[key])

for key in jugador.keys():
    print(key)

for value in jugador.values():
    print(value)

Vectores y Tipos Especiales

# Vectores 2D
var posicion: Vector2 = Vector2(100, 200)
var direccion = Vector2.UP  # (0, -1)
var velocidad = Vector2.ZERO  # (0, 0)

# Operaciones con vectores
var suma = posicion + direccion * 10
var distancia = posicion.distance_to(Vector2.ZERO)
var normalizado = direccion.normalized()
var longitud = velocidad.length()

# Vectores 3D
var posicion_3d: Vector3 = Vector3(1, 2, 3)
var forward = Vector3.FORWARD  # (0, 0, -1)

# Colores
var rojo: Color = Color.RED
var custom = Color(0.5, 0.7, 1.0, 0.8)  # RGBA
var hex_color = Color("#FF5733")

# Rect2 (Rectángulos)
var area: Rect2 = Rect2(0, 0, 100, 50)  # x, y, width, height
var centro = area.get_center()
var contiene = area.has_point(Vector2(50, 25))

# Transform2D (Transformaciones)
var transform: Transform2D = Transform2D()
transform = transform.rotated(PI/4)  # Rotar 45 grados
transform = transform.scaled(Vector2(2, 2))  # Escalar 2x

# NodePath (Rutas de nodos)
var ruta: NodePath = NodePath("Player/Sprite2D")
var ruta_absoluta = NodePath("/root/Main/Player")

Estructuras de Control

Condicionales (if/elif/else)

var vida = 75
var mana = 30

# If simple
if vida > 0:
    print("Estás vivo")

# If/else
if vida >= 100:
    print("Vida completa")
else:
    print("Vida parcial")

# If/elif/else
if vida >= 100:
    print("Vida completa")
elif vida >= 50:
    print("Vida buena")
elif vida >= 25:
    print("Vida baja")
else:
    print("Crítico")

# Operador ternario (inline if)
var estado = "Vivo" if vida > 0 else "Muerto"

# Condiciones complejas
if vida > 0 and mana > 0:
    print("Puedes lanzar hechizos")

if vida < 25 or mana == 0:
    print("Necesitas descansar")

if not esta_muerto:
    print("Sigues en el juego")

# Match (similar a switch)
var arma = "espada"

match arma:
    "espada":
        print("Daño: 10")
    "arco":
        print("Daño: 7")
    "magia":
        print("Daño: 15")
    _:  # Default
        print("Arma desconocida")

# Match con patrones
var estado_jugador = {"vida": 100, "mana": 50}

match estado_jugador:
    {"vida": 100, "mana": var m}:
        print("Vida llena, mana: ", m)
    {"vida": var v, "mana": 0}:
        print("Sin mana, vida: ", v)
    {"vida": var v, "mana": var m} when v < 25:
        print("Vida crítica")
    _:
        print("Estado normal")

Bucles (Loops)

# For con rango
for i in range(5):
    print(i)  # 0, 1, 2, 3, 4

for i in range(2, 10):
    print(i)  # 2, 3, 4... 9

for i in range(10, 0, -2):
    print(i)  # 10, 8, 6, 4, 2

# For con array
var enemigos = ["Goblin", "Orco", "Dragón"]
for enemigo in enemigos:
    print("Luchando contra: ", enemigo)

# For con índice y valor
for i in range(enemigos.size()):
    print(i, ": ", enemigos[i])

# While
var contador = 0
while contador < 5:
    print(contador)
    contador += 1

# Do-while equivalente
var intentos = 0
while true:
    print("Intento: ", intentos)
    intentos += 1
    if intentos >= 3:
        break

# Break y continue
for i in range(10):
    if i == 3:
        continue  # Salta esta iteración
    if i == 7:
        break  # Sale del bucle
    print(i)

# Bucles anidados
for i in range(3):
    for j in range(3):
        print("Posición: ", i, ", ", j)

Funciones

Definición y Llamada

# Función simple
func saludar():
    print("¡Hola!")

# Función con parámetros
func saludar_a(nombre: String):
    print("¡Hola, ", nombre, "!")

# Función con valor de retorno
func sumar(a: int, b: int) -> int:
    return a + b

# Función con parámetros opcionales
func crear_personaje(nombre: String, clase: String = "Guerrero", nivel: int = 1):
    print("Personaje: ", nombre, " - ", clase, " Nivel ", nivel)

# Llamadas a funciones
saludar()
saludar_a("Jugador")
var resultado = sumar(5, 3)
crear_personaje("Hero")
crear_personaje("Mago", "Mago", 5)
crear_personaje("Elfo", "Arquero")  # Usa nivel por defecto

# Función con múltiples returns
func dividir(a: float, b: float) -> float:
    if b == 0:
        push_error("División por cero")
        return 0.0
    return a / b

# Función que modifica parámetros
func duplicar_array(arr: Array) -> void:
    for i in range(arr.size()):
        arr[i] *= 2

Funciones como Variables (First-Class Functions)

# Guardar función en variable
var mi_funcion = func(x): return x * 2

# Llamar función almacenada
var resultado = mi_funcion.call(5)  # 10

# Pasar función como parámetro
func aplicar_operacion(numeros: Array, operacion: Callable):
    var resultados = []
    for num in numeros:
        resultados.append(operacion.call(num))
    return resultados

# Usar con lambda
var cuadrados = aplicar_operacion([1, 2, 3], func(x): return x * x)
print(cuadrados)  # [1, 4, 9]

# Callbacks
func hacer_algo_async(callback: Callable):
    # Simular operación async
    await get_tree().create_timer(1.0).timeout
    callback.call("Operación completada")

# Uso
hacer_algo_async(func(msg): print(msg))

Funciones Async/Await

# Función async con await
func cargar_datos() -> void:
    print("Cargando...")
    await get_tree().create_timer(2.0).timeout
    print("¡Datos cargados!")

# Función que retorna valor async
func obtener_datos_async() -> String:
    await get_tree().create_timer(1.0).timeout
    return "Datos importantes"

# Uso
func _ready():
    await cargar_datos()
    var datos = await obtener_datos_async()
    print(datos)

# Await con señales
func esperar_click():
    print("Esperando click...")
    await self.gui_input
    print("¡Click detectado!")

Programación Orientada a Objetos

Clases y Herencia

# Archivo: personaje.gd
class_name Personaje
extends Node2D

# Propiedades
var nombre: String
var vida: int = 100
var nivel: int = 1

# Constructor
func _init(p_nombre: String = "Sin Nombre"):
    nombre = p_nombre

# Métodos
func recibir_dano(cantidad: int) -> void:
    vida -= cantidad
    if vida <= 0:
        morir()

func morir() -> void:
    print(nombre, " ha muerto")
    queue_free()

# Método virtual (para sobrescribir)
func atacar() -> int:
    return 10  # Daño base
# Archivo: guerrero.gd
class_name Guerrero
extends Personaje

var fuerza: int = 15
var armadura: int = 10

func _init():
    super("Guerrero")  # Llama al constructor padre
    vida = 150  # Más vida que personaje base

# Sobrescribir método
func atacar() -> int:
    return super.atacar() + fuerza  # Daño base + fuerza

# Método nuevo
func golpe_poderoso() -> int:
    return atacar() * 2

Propiedades con Getters y Setters

class_name Jugador
extends CharacterBody2D

# Variable privada (convención con _)
var _vida: int = 100

# Propiedad con setter personalizado
var vida: int:
    get:
        return _vida
    set(value):
        _vida = clamp(value, 0, 100)
        vida_cambiada.emit(_vida)

# Propiedad solo lectura
var esta_muerto: bool:
    get:
        return _vida <= 0

# Signal para notificar cambios
signal vida_cambiada(nueva_vida)

# Propiedad con setter que valida
var nivel: int = 1:
    set(value):
        if value > 0 and value <= 100:
            nivel = value
            actualizar_stats()

func actualizar_stats():
    # Actualizar estadísticas basadas en nivel
    pass

Composición vs Herencia

# Componente de Salud (composición)
class_name ComponenteSalud
extends Node

var vida_maxima: int = 100
var vida_actual: int = 100

signal muerto()
signal vida_cambiada(nueva_vida)

func recibir_dano(cantidad: int):
    vida_actual -= cantidad
    vida_cambiada.emit(vida_actual)
    if vida_actual <= 0:
        muerto.emit()

func curar(cantidad: int):
    vida_actual = min(vida_actual + cantidad, vida_maxima)
    vida_cambiada.emit(vida_actual)
# Usar composición
extends CharacterBody2D

@onready var salud: ComponenteSalud = $ComponenteSalud
@onready var inventario: ComponenteInventario = $ComponenteInventario

func _ready():
    salud.muerto.connect(_on_muerte)

func _on_muerte():
    print("Game Over")
    queue_free()

Sistema de Señales (Signals)

Las señales son el sistema de eventos de Godot, permitiendo comunicación desacoplada entre nodos.

Definir y Emitir Señales

extends Node

# Señal simple
signal explosion()

# Señal con parámetros
signal vida_cambiada(nueva_vida: int)
signal item_recogido(item_nombre: String, cantidad: int)

# Emitir señales
func explotar():
    print("¡BOOM!")
    explosion.emit()

func cambiar_vida(nueva: int):
    vida_cambiada.emit(nueva)

func recoger_item(nombre: String):
    item_recogido.emit(nombre, 1)

Conectar Señales

extends Node2D

@onready var jugador = $Jugador
@onready var ui = $UI

func _ready():
    # Conectar con método
    jugador.vida_cambiada.connect(_on_jugador_vida_cambiada)
    
    # Conectar con lambda
    jugador.explosion.connect(func(): print("Explosión detectada"))
    
    # Conectar one-shot (se desconecta después de una emisión)
    jugador.muerto.connect(_on_jugador_muerto, CONNECT_ONE_SHOT)
    
    # Conectar con bind (pasar parámetros extra)
    jugador.item_recogido.connect(_on_item_recogido.bind("Extra Info"))

func _on_jugador_vida_cambiada(nueva_vida: int):
    ui.actualizar_vida(nueva_vida)

func _on_jugador_muerto():
    get_tree().reload_current_scene()

func _on_item_recogido(nombre: String, cantidad: int, info: String):
    print("Item: ", nombre, " x", cantidad, " - ", info)

# Desconectar señales
func _exit_tree():
    jugador.vida_cambiada.disconnect(_on_jugador_vida_cambiada)

Señales Built-in

extends Button

func _ready():
    # Señales de UI
    pressed.connect(_on_button_pressed)
    mouse_entered.connect(_on_mouse_entered)
    
    # Timer signals
    var timer = Timer.new()
    add_child(timer)
    timer.timeout.connect(_on_timer_timeout)
    timer.start(1.0)
    
    # Area2D signals
    var area = Area2D.new()
    area.body_entered.connect(_on_body_entered)
    area.area_entered.connect(_on_area_entered)

func _on_button_pressed():
    print("Botón presionado")

func _on_mouse_entered():
    modulate = Color.YELLOW

func _on_timer_timeout():
    print("Timer!")

func _on_body_entered(body: Node2D):
    if body.is_in_group("enemies"):
        body.recibir_dano(10)

Manejo de Errores

# Assertions (solo en debug)
func dividir(a: float, b: float) -> float:
    assert(b != 0, "División por cero no permitida")
    return a / b

# Push error (aparece en debugger)
func cargar_archivo(ruta: String):
    var archivo = FileAccess.open(ruta, FileAccess.READ)
    if archivo == null:
        push_error("No se pudo abrir el archivo: " + ruta)
        return null
    return archivo.get_as_text()

# Try-catch equivalente
func operacion_riesgosa():
    var resultado = hacer_algo()
    if resultado == null:
        printerr("Error en operación")
        return ERR_FAILED
    return OK

# Manejo de errores con enums
enum ErrorCodigo {
    OK,
    ERROR_ARCHIVO,
    ERROR_RED,
    ERROR_DATOS
}

func procesar() -> ErrorCodigo:
    var archivo = cargar_archivo("data.json")
    if archivo == null:
        return ErrorCodigo.ERROR_ARCHIVO
    
    var datos = parsear_json(archivo)
    if datos == null:
        return ErrorCodigo.ERROR_DATOS
    
    return ErrorCodigo.OK

# Uso
match procesar():
    ErrorCodigo.OK:
        print("Éxito")
    ErrorCodigo.ERROR_ARCHIVO:
        print("Error al cargar archivo")
    ErrorCodigo.ERROR_DATOS:
        print("Error al parsear datos")

Anotaciones y Exports

Las anotaciones (@) proporcionan metadata y funcionalidad especial:

extends Node2D

# @export hace las variables visibles en el Inspector
@export var velocidad: float = 100.0
@export var vida_maxima: int = 100
@export var nombre_jugador: String = "Hero"

# Export con rangos
@export_range(0, 100, 1) var porcentaje: int = 50
@export_range(0.0, 1.0, 0.01) var volumen: float = 0.5

# Export enums
enum TipoArma { ESPADA, ARCO, MAGIA }
@export var arma: TipoArma = TipoArma.ESPADA

# Export de recursos
@export var textura: Texture2D
@export var sonido: AudioStream
@export var escena_enemigo: PackedScene

# Export de archivos
@export_file var archivo_config: String
@export_file("*.json") var archivo_json: String
@export_dir var carpeta: String

# Export de colores
@export var color_principal: Color = Color.WHITE
@export_color_no_alpha var color_fondo: Color

# Export de NodePaths
@export var objetivo: NodePath = ^"../Target"

# Export de arrays
@export var items: Array[String] = ["Poción", "Espada"]
@export var enemigos: Array[PackedScene] = []

# Export grupos
@export_group("Configuración de Combate")
@export var ataque: int = 10
@export var defensa: int = 5
@export_group("") # Fin del grupo

# Export subgrupos
@export_subgroup("Estadísticas Avanzadas")
@export var critico: float = 0.1
@export var esquiva: float = 0.05

# @onready ejecuta cuando el nodo está listo
@onready var sprite = $Sprite2D
@onready var animation = $AnimationPlayer
@onready var collision = $CollisionShape2D

# @tool hace que el script funcione en el editor
@tool
extends EditorScript

# @icon añade un icono personalizado al nodo
@icon("res://icons/custom_node.png")
class_name MiNodoPersonalizado

Input y Controles

Detectar Input

extends Node

func _input(event):
    # Mouse
    if event is InputEventMouseButton:
        if event.button_index == MOUSE_BUTTON_LEFT:
            if event.pressed:
                print("Click izquierdo en: ", event.position)
    
    if event is InputEventMouseMotion:
        print("Mouse movido a: ", event.position)
    
    # Teclado
    if event is InputEventKey:
        if event.pressed:
            print("Tecla presionada: ", event.keycode)
        if event.keycode == KEY_SPACE and event.pressed:
            print("¡Espacio!")
    
    # Touch/Pantalla táctil
    if event is InputEventScreenTouch:
        if event.pressed:
            print("Toque en: ", event.position)
    
    if event is InputEventScreenDrag:
        print("Arrastre: ", event.relative)

func _process(delta):
    # Input continuo (polling)
    if Input.is_action_pressed("ui_right"):
        position.x += 100 * delta
    
    if Input.is_action_just_pressed("jump"):
        print("¡Salto iniciado!")
    
    if Input.is_action_just_released("jump"):
        print("¡Salto terminado!")
    
    # Mouse
    var mouse_pos = get_global_mouse_position()
    if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
        look_at(mouse_pos)
    
    # Teclado directo
    if Input.is_key_pressed(KEY_SHIFT):
        velocidad *= 2
    
    # Gamepad
    var joy_vector = Input.get_vector("move_left", "move_right", "move_up", "move_down")
    velocity = joy_vector * speed

Input Actions (Acciones)

Configura en Project Settings → Input Map:

# Definir acciones en código (opcional, mejor en Project Settings)
func _ready():
    # Estas normalmente se configuran en el editor
    InputMap.add_action("atacar")
    var evento = InputEventKey.new()
    evento.keycode = KEY_Z
    InputMap.action_add_event("atacar", evento)

# Usar acciones
func _process(delta):
    if Input.is_action_pressed("move_right"):
        position.x += speed * delta
    
    if Input.is_action_just_pressed("jump"):
        if is_on_floor():
            velocity.y = -jump_force
    
    if Input.is_action_just_pressed("atacar"):
        realizar_ataque()
    
    # Obtener fuerza de acción (para analog sticks)
    var move_strength = Input.get_action_strength("move_right")
    position.x += speed * move_strength * delta
    
    # Vector de movimiento 2D
    var input_vector = Input.get_vector(
        "move_left", "move_right",
        "move_up", "move_down"
    )
    velocity = input_vector * speed

Trabajando con el Árbol de Escena

extends Node2D

func _ready():
    # Obtener nodos
    var sprite = get_node("Sprite2D")  # Ruta relativa
    var player = get_node("/root/Main/Player")  # Ruta absoluta
    var parent = get_parent()
    var root = get_tree().root
    
    # Notación $ (shorthand para get_node)
    var animation = $AnimationPlayer
    var nested = $"Container/Child"
    
    # Buscar nodos
    var timer = get_node_or_null("Timer")
    if timer:
        timer.start()
    
    # Obtener todos los nodos de un tipo
    var enemies = get_tree().get_nodes_in_group("enemies")
    
    # Crear nodos dinámicamente
    var nuevo_enemigo = load("res://scenes/enemy.tscn").instantiate()
    add_child(nuevo_enemigo)
    nuevo_enemigo.global_position = Vector2(100, 100)
    
    # Eliminar nodos
    queue_free()  # Elimina al final del frame (seguro)
    # o
    get_parent().remove_child(self)  # Elimina inmediatamente
    
    # Cambiar escena
    get_tree().change_scene_to_file("res://scenes/menu.tscn")
    
    # Recargar escena actual
    get_tree().reload_current_scene()
    
    # Pausar juego
    get_tree().paused = true
    
    # Grupos
    add_to_group("enemies")
    remove_from_group("enemies")
    if is_in_group("enemies"):
        print("Es un enemigo")
    
    # Llamar método en todos los nodos de un grupo
    get_tree().call_group("enemies", "recibir_dano", 10)

Sistema de Recursos

# Cargar recursos
var textura = load("res://sprites/player.png")  # Carga inmediata
var textura_async = preload("res://sprites/enemy.png")  # Carga en compile time

# Cargar escenas
var escena_enemigo = load("res://scenes/enemy.tscn")
var instancia = escena_enemigo.instantiate()

# Recursos como exports
@export var mi_textura: Texture2D
@export var mi_sonido: AudioStream

# Crear recursos en código
func crear_textura():
    var imagen = Image.create(100, 100, false, Image.FORMAT_RGB8)
    imagen.fill(Color.RED)
    var textura = ImageTexture.create_from_image(imagen)
    return textura

# Guardar recursos
func guardar_datos():
    var save_game = FileAccess.open("user://savegame.save", FileAccess.WRITE)
    if save_game:
        var datos = {
            "nivel": 5,
            "vida": 80,
            "nombre": "Hero"
        }
        save_game.store_var(datos)
        save_game.close()

# Cargar recursos guardados
func cargar_datos():
    if FileAccess.file_exists("user://savegame.save"):
        var save_game = FileAccess.open("user://savegame.save", FileAccess.READ)
        var datos = save_game.get_var()
        save_game.close()
        return datos
    return null

Tips y Mejores Prácticas

Convenciones de Nomenclatura

# Constantes: MAYUSCULAS_CON_GUIONES
const MAX_VIDA = 100
const VELOCIDAD_INICIAL = 50.0

# Variables: snake_case
var vida_actual = 100
var puede_saltar = true

# Funciones: snake_case
func calcular_dano():
    pass

# Señales: snake_case, tiempo pasado
signal enemigo_derrotado
signal item_recogido

# Clases: PascalCase
class_name SistemaDeVida
class_name ControladorJugador

# Enums: PascalCase para el enum, MAYUSCULAS para valores
enum EstadoJuego {
    MENU,
    JUGANDO,
    PAUSADO,
    GAME_OVER
}

# Nodos en escena: PascalCase
# Player, HealthBar, EnemySpawner

# Archivos: snake_case
# player_controller.gd, sistema_vida.gd

Optimización y Performance

# Cachear referencias de nodos
@onready var sprite = $Sprite2D  # Se ejecuta una vez
# En lugar de:
func _process(delta):
    $Sprite2D.rotation += delta  # Busca el nodo cada frame
    
# Mejor:
@onready var sprite = $Sprite2D
func _process(delta):
    sprite.rotation += delta  # Usa referencia cacheada

# Usar señales en lugar de polling
# Malo:
func _process(delta):
    if vida <= 0:
        morir()

# Bueno:
signal muerto
var vida: int:
    set(value):
        vida = value
        if vida <= 0:
            muerto.emit()

# Usar object pooling para objetos frecuentes
var pool_balas = []
var max_balas = 100

func _ready():
    for i in max_balas:
        var bala = preload("res://bala.tscn").instantiate()
        bala.set_process(false)
        bala.visible = false
        add_child(bala)
        pool_balas.append(bala)

func disparar():
    for bala in pool_balas:
        if not bala.visible:
            bala.global_position = global_position
            bala.visible = true
            bala.set_process(true)
            break

Debugging

# Print para debugging
print("Valor: ", valor)
print_debug("Solo en debug mode")
printerr("Mensaje de error")
print_rich("[color=red]Texto en color[/color]")

# Breakpoints
# Haz click en el número de línea en el editor para añadir breakpoint

# Asserts
assert(vida >= 0, "La vida no puede ser negativa")

# Profiling
var tiempo_inicio = Time.get_ticks_msec()
# ... código a medir ...
var tiempo_fin = Time.get_ticks_msec()
print("Tiempo: ", tiempo_fin - tiempo_inicio, " ms")

# Inspector personalizado
@export var debug_mode = false
@export_group("Debug")
@export var mostrar_colisiones = false
@export var mostrar_paths = false

func _draw():
    if debug_mode and mostrar_colisiones:
        draw_circle(Vector2.ZERO, 50, Color.RED)

Ejercicios Prácticos

Ejercicio 1: Sistema de Inventario

Crea un sistema de inventario con las siguientes características:

Ejercicio 2: Sistema de Diálogos

Implementa un sistema de diálogos que:

Ejercicio 3: Estado de Enemigo

Crea un enemigo con estados:

Ejercicio 4: Power-Ups

Sistema de power-ups temporales:

Resumen del Capítulo

Has aprendido los fundamentos de GDScript:

Próximo Capítulo

En el siguiente capítulo crearemos nuestro primer juego completo: ¡Pong!

→ Capítulo 3: Tu Primer Juego - Pong


Desafío: Crea una clase Vehiculo con subclases Coche, Moto y Bicicleta. Cada una debe tener velocidad máxima diferente y método acelerar() único. ¡Compártelo en Discord!