Parte: 11 — Móvil, consolas y plataformas · Fuente: Documentación de Godot 4 (Input) y guías de logros/nube de plataformas ⏱️ Duración estimada: 80 min · Nivel: Intermedio
Trabajar los tres pilares de la experiencia en consola desde Godot 4: el input de gamepad (que Godot sí maneja de forma nativa mediante su sistema de joypad), la mostrar los glifos correctos de botones según la plataforma, y la abstracción del guardado para que soporte tanto disco local como guardado en la nube del ecosistema (trofeos/logros y saved games los expone el porteador/servicio, no el motor).
La idea clave: aunque los logros y la nube de cada consola llegan por una capa externa, tú puedes preparar el juego para todo ello desde ahora con código Godot puro: detectar el mando con Input.get_connected_joypads(), identificarlo con Input.get_joy_name() para elegir el set de glifos, y encapsular el guardado tras una interfaz que luego enchufa la nube. Al terminar tendrás un sistema de glifos por plataforma y un guardado abstracto listo para cloud.
Al finalizar, el alumno podrá:
| # | Tema | Por qué importa |
|---|---|---|
| 1 | Joypad nativo en Godot | El input de mando sí es API del motor. |
| 2 | Detección de mando | Saber si hay mando y cuál define la UI. |
| 3 | Glifos por plataforma | Un icono de botón erróneo confunde y falla certificación. |
| 4 | Input Map por acciones | Desacopla la lógica de los botones físicos. |
| 5 | Trofeos/logros del ecosistema | Capa social propia de cada consola. |
| 6 | Guardado en nube | Continuidad entre sesiones y dispositivos. |
| 7 | Abstracción de guardado | Permite cambiar el backend sin tocar el juego. |
| 8 | Suspensión/resume y estado | El guardado debe ser robusto ante interrupciones. |
InputEventJoypadButton/Motion. Clave: no necesita plugin para leerse.Input.get_connected_joypads(): devuelve la lista de IDs de mandos conectados. Clave: base para detectar presencia de mando.Input.get_joy_name(id): nombre del mando (p. ej. "Xbox", "PS5", "Switch Pro"). Clave: permite inferir qué familia de glifos usar.Input.is_action_pressed("saltar"), no un botón concreto.guardar/cargar) independiente del backend. Clave: hoy escribe a disco; mañana, a la nube.Necesitas Godot 4.x y, si es posible, un mando físico (Xbox, PlayStation o Switch Pro) para probar la detección real. Prepara un pequeño set de iconos de botones por familia (Xbox y PlayStation como mínimo); para el laboratorio bastan marcadores de texto ("A", "✕") si no tienes arte.
Configura primero el Input Map en Project → Project Settings → Input Map con acciones como saltar, disparar, pausa, y asígnales tanto teclas como botones de mando. Documentación: Input de Godot https://docs.godotengine.org/en/stable/tutorials/inputs/index.html y mandos https://docs.godotengine.org/en/stable/tutorials/inputs/controllers_gamepads_joysticks.html.
Detectaremos el mando, elegiremos glifos por plataforma y abstraeremos el guardado para soportar nube.
Glifos que detecte la familia del mando:extends Node
enum Familia { TECLADO, XBOX, PLAYSTATION, NINTENDO }
var familia_actual: Familia = Familia.TECLADO
func _ready() -> void:
Input.joy_connection_changed.connect(func(_id, _con): _actualizar())
_actualizar()
func _actualizar() -> void:
var mandos := Input.get_connected_joypads()
if mandos.is_empty():
familia_actual = Familia.TECLADO
return
var nombre := Input.get_joy_name(mandos[0]).to_lower()
if "xbox" in nombre or "xinput" in nombre:
familia_actual = Familia.XBOX
elif "ps" in nombre or "dualsense" in nombre or "playstation" in nombre or "dualshock" in nombre:
familia_actual = Familia.PLAYSTATION
elif "switch" in nombre or "nintendo" in nombre:
familia_actual = Familia.NINTENDO
else:
familia_actual = Familia.XBOX # Xbox como estilo por defecto
func glifo_confirmar() -> String:
# El botón "confirmar" cambia de símbolo según la plataforma.
match familia_actual:
Familia.PLAYSTATION: return "✕"
Familia.NINTENDO: return "A" # Nintendo intercambia A/B respecto a Xbox
Familia.XBOX: return "A"
_: return "Enter"
func _process(_delta: float) -> void:
$UI/Ayuda.text = "Pulsa %s para saltar" % Glifos.glifo_confirmar()
func _physics_process(_delta: float) -> void:
if Input.is_action_just_pressed("saltar"):
_saltar()
res://guardado/almacen.gd:class_name Almacen
extends RefCounted
# Contrato de guardado. Las implementaciones concretas (disco, nube)
# heredan de aquí; el juego solo conoce esta interfaz.
func guardar(_datos: Dictionary) -> bool:
push_error("guardar() no implementado")
return false
func cargar() -> Dictionary:
push_error("cargar() no implementado")
return {}
res://guardado/almacen_disco.gd:class_name AlmacenDisco
extends Almacen
const RUTA := "user://partida.save"
func guardar(datos: Dictionary) -> bool:
# Escritura segura: temporal + renombrado para no corromper la partida.
var tmp := RUTA + ".tmp"
var f := FileAccess.open(tmp, FileAccess.WRITE)
if f == null:
return false
f.store_string(JSON.stringify(datos))
f.close()
DirAccess.rename_absolute(ProjectSettings.globalize_path(tmp),
ProjectSettings.globalize_path(RUTA))
return true
func cargar() -> Dictionary:
if not FileAccess.file_exists(RUTA):
return {}
var f := FileAccess.open(RUTA, FileAccess.READ)
var texto := f.get_as_text()
f.close()
var datos = JSON.parse_string(texto)
return datos if datos is Dictionary else {}
Guardado, elige el backend. Hoy es disco; el día del port, se sustituye por un AlmacenNube que hable con el servicio del ecosistema, sin tocar el resto del juego:extends Node
var _almacen: Almacen = AlmacenDisco.new()
func guardar_partida(datos: Dictionary) -> void:
_almacen.guardar(datos)
func cargar_partida() -> Dictionary:
return _almacen.cargar()
TextureRect) según la familia.AlmacenNube de mentira (mock) que herede de Almacen y registre las llamadas por consola.Implementa un sistema que (a) muestre el glifo de confirmar correcto según el mando conectado (al menos Xbox, PlayStation y teclado), usando Input.get_connected_joypads() y Input.get_joy_name(), y (b) guarde y cargue la partida a través de una interfaz Almacen con un backend de disco que use escritura segura. Deja preparado un segundo backend (mock de nube) intercambiable sin modificar el juego.
Criterio de aceptación: al cambiar de mando, el glifo mostrado se actualiza correctamente; guardar y volver a cargar restaura los datos; y cambiar el backend de AlmacenDisco a la implementación mock no requiere tocar la lógica del juego.
| Síntoma / mensaje | Causa y cómo arreglar |
|---|---|
| El juego siempre muestra glifos de Xbox | No consultas get_joy_name() o no reaccionas a joy_connection_changed. |
| El botón "confirmar" queda invertido en Nintendo | No contemplaste que Nintendo intercambia A/B respecto a Xbox. Ajusta el mapeo. |
| La lógica se rompe con otro mando | Usaste códigos de botón fijos en vez de acciones del Input Map. |
| Partida corrupta al guardar | Escribes directo sobre el archivo. Usa temporal + renombrado. |
| Migrar a nube exige reescribir el juego | El guardado no estaba abstraído. Encapsúlalo tras Almacen. |
❓ ¿El input de mando necesita plugin en Godot? No. Godot maneja gamepads de forma nativa; los eventos de joypad y las acciones del Input Map funcionan sin plugins.
❓ ¿Cómo sé qué glifos mostrar? Consulta Input.get_joy_name() del mando conectado e infiere la familia (Xbox, PlayStation, Nintendo) para elegir el set de iconos.
❓ ¿Los logros/trofeos se programan en Godot? La condición de desbloqueo sí la decides tú; el desbloqueo real en el ecosistema de consola lo expone la capa nativa del porteador/servicio.
❓ ¿Por qué abstraer el guardado? Porque la nube de cada consola es un backend distinto. Si el juego solo conoce la interfaz Almacen, cambiar a nube no toca la lógica.
Clase 210 - Certificación (TRC/TCR) y requisitos de consola