Capítulo 2: GDScript - Fundamentos del Lenguaje
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
- Sintaxis tipo Python: Indentación significativa, sin llaves ni punto y coma
- Integrado con Godot: Acceso directo a todas las clases y funciones del engine
- Hot Reload: Cambios en tiempo real sin reiniciar el juego
- Tipado opcional: Puedes añadir tipos para mejor performance y autocompletado
- Orientado a objetos: Herencia, polimorfismo, encapsulación
- Garbage collected: No te preocupes por gestión de memoria
¿Por Qué GDScript y No C# o C++?
| Aspecto | GDScript | C# | 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:
- Integración con librerías .NET existentes (C#)
- Máximo rendimiento en simulaciones complejas (C++)
- Portar código existente de otros engines
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:
- Array de items con límite de capacidad
- Funciones para añadir, quitar y usar items
- Señales cuando se añade/quita un item
- Persistencia (guardar/cargar)
Ejercicio 2: Sistema de Diálogos
Implementa un sistema de diálogos que:
- Muestre texto carácter por carácter
- Soporte múltiples opciones de respuesta
- Use señales para notificar cuando termina
- Permita saltar el texto con una tecla
Ejercicio 3: Estado de Enemigo
Crea un enemigo con estados:
- Idle, Patrullando, Persiguiendo, Atacando
- Transiciones basadas en distancia al jugador
- Usa match para manejar estados
- Implementa con señales y funciones
Ejercicio 4: Power-Ups
Sistema de power-ups temporales:
- Diferentes tipos (velocidad, daño, escudo)
- Duración con timer
- Stackeable o no stackeable según tipo
- Efectos visuales cuando está activo
Resumen del Capítulo
Has aprendido los fundamentos de GDScript:
- ✅ Variables y tipos de datos
- ✅ Estructuras de control (if, for, while, match)
- ✅ Funciones y parámetros
- ✅ Programación orientada a objetos
- ✅ Sistema de señales
- ✅ Manejo de input
- ✅ Trabajo con el árbol de escena
- ✅ Mejores prácticas y optimización
Próximo Capítulo
En el siguiente capítulo crearemos nuestro primer juego completo: ¡Pong!
- Aplicaremos todo lo aprendido
- Física y colisiones
- UI y puntuación
- Efectos de sonido
- Menú principal
→ 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!