Saltar al contenido principal

3. Crea el trigger de la puerta y el script Lua

La puerta se abrirá cuando el jugador atraviese un volumen invisible. Un solo script Lua administrará el trigger, la posición del punto de control, el estado persistente de la puerta, los callbacks de la interfaz y el proceso de reinicio.

Crea el GameObject del trigger​

  1. Selecciona Scene > Create Entity > Create Empty y mantenlo en la raíz de la escena, separado de Door y Player. Desactivar Door no debe desactivar su controlador.
  2. Cambia el nombre del GameObject a Door Trigger.
  3. Establece Position de su Transform en (0, 1.0, -2.25).
  4. En el Inspector, selecciona Add Component > Collider.
  5. Establece Type en AABB y Size en (1.6, 2.0, 1.0). Mantén presionada la tecla Ctrl y haz clic en un campo numérico para introducir un valor exacto; después presiona Enter.
  6. Activa Is Trigger.

El trigger queda justo antes de la puerta al acercarte desde Room A. Debe superponerse con la cápsula del jugador sin bloquear su movimiento.

Door Trigger seleccionado en la escena de dos habitaciones, con sus opciones de Lua, audio y collider de trigger visibles

Door Trigger terminado queda del lado de Room A de la abertura. El Inspector muestra los enlaces por nombre del script, el sonido del punto de control y las dimensiones del trigger. El audio se agrega en el siguiente capítulo.

Abre la imagen para verla a tamaño completo.

Crea y asigna el script​

  1. Con Door Trigger seleccionado, elige Add Component > Lua Behaviour.
  2. Se abrirá el cuadro de diálogo Select Lua Script. Selecciona Create New. Si ya agregaste un Lua Behaviour vacío, su botón Create Script... abre el mismo flujo de creación.
  3. Asígnale el nombre tutorial_controller.lua, selecciona la plantilla Behaviour, elige Assets/Scripts como ubicación y selecciona Create.
  4. Selecciona Open Script.
  5. Reemplaza el contenido generado con el siguiente código y guarda el archivo.

También puedes descargar el script completo y copiarlo sobre el archivo generado en Assets/Scripts/tutorial_controller.lua. Regresa al engine y confirma que Lua Behaviour muestre el script como PS1 Ready.

-- Estas propiedades del Inspector enlazan el script con los objetos que creas.
-- Escribe sus nombres exactamente; no los traduzcas ni los repitas en otros objetos.
playerName = "Player"
doorName = "Door"
statusTextName = "Status Text"
-- Agrupa los datos de este acertijo bajo un mismo prefijo. Si lo cambias, el
-- script buscará otro punto de control; las claves guardadas no se renombrarán.
savePrefix = "two_room_relay"

-- Busca los nombres una sola vez en Start y reutiliza los ID en los callbacks.
-- El HUD se agrega en el siguiente capítulo, así que su ID puede no existir aún.
local playerId = -1
local doorId = -1
local statusId = -1
-- Reset devuelve al jugador a esta posición, no al último punto de control.
local startPosition = nil
-- Lleva el estado actual de la puerta por separado del progreso guardado:
-- puede permanecer abierta aunque la Memory Card no esté disponible.
local doorOpen = false
-- Estos cuatro valores forman el punto de control completo del tutorial.
local fields = { "x", "y", "z", "door_open" }

-- Separa las claves de este acertijo de las que usan otros sistemas en PlayerPrefs.
local function key(name)
return savePrefix .. "." .. name
end

local function status(message)
-- Conserva el diagnóstico completo en la Console, incluso antes de crear el HUD.
Log.Print(message)
if statusId >= 0 then
-- Ajusta los mensajes ASCII del tutorial al límite de texto de PS1: 62 caracteres
-- y un salto de línea. Dos líneas de hasta 32 caracteres caben en este HUD.
local display = string.sub(message, 1, 62)
if #display > 32 then
display = string.sub(display, 1, 32) .. "\n" .. string.sub(display, 33)
end
Text.SetText(statusId, display)
end
end

-- Detén las acciones y el guardado si falta un objeto enlazado por nombre.
-- Door sigue existiendo al desactivarse, así que una puerta abierta es válida.
local function valid_targets()
if not GameObject.Exists(playerId) or not GameObject.Exists(doorId) then
status("SETUP ERROR: CHECK PLAYER AND DOOR NAMES")
return false
end
return true
end

-- Abrir esta puerta desactiva tanto su malla como el collider que bloquea el paso.
-- Door Trigger sigue activo porque es un GameObject independiente.
local function set_door(open)
if not GameObject.SetActive(doorId, not open) then
status("SETUP ERROR: DOOR IS UNAVAILABLE")
return false
end
doorOpen = open
return true
end

-- Conserva una copia de los valores existentes del tutorial por si falla una
-- operación. Respeta sus tipos: las coordenadas son float y door_open es int.
local function capture_preferences()
local previous = {}
for _, name in ipairs(fields) do
if PlayerPrefs.HasKey(key(name)) then
previous[name] = name == "door_open"
and PlayerPrefs.GetInt(key(name), 0)
or PlayerPrefs.GetFloat(key(name), 0.0)
end
end
return previous
end

-- Borra de memoria solo las claves del acertijo; conserva los demás datos.
-- Quien llama esta función debe ejecutar Save para guardar el reinicio.
local function clear_preferences()
for _, name in ipairs(fields) do
-- Si la clave no existe, devuelve false, pero ya no hay nada que borrar.
PlayerPrefs.DeleteKey(key(name))
end
end

-- Deshaz los cambios en memoria si falla una operación. Primero elimina las
-- claves parciales para no reutilizar un punto incompleto ni ocupar más entradas.
local function restore_preferences(previous)
clear_preferences()
for _, name in ipairs(fields) do
local value = previous[name]
if value ~= nil then
if name == "door_open" then
PlayerPrefs.SetInt(key(name), value)
else
PlayerPrefs.SetFloat(key(name), value)
end
end
end
end

local function save_checkpoint(hostId, message)
if not valid_targets() then return false end
local position = Transform.GetPosition(playerId)
if position == nil then
status("SAVE FAILED: PLAYER IS UNAVAILABLE")
return false
end
-- Prepara en memoria la posición y el estado de la puerta como un solo punto
-- de control. Si falla un valor, detén la escritura para no guardar datos parciales.
local previous = capture_preferences()
local written = PlayerPrefs.SetFloat(key("x"), position.x)
and PlayerPrefs.SetFloat(key("y"), position.y)
and PlayerPrefs.SetFloat(key("z"), position.z)
and PlayerPrefs.SetInt(key("door_open"), doorOpen and 1 or 0)
-- SetFloat y SetInt no guardan en el dispositivo. Save escribe los datos
-- en el archivo local del editor o en la Memory Card del puerto 1 de PS1.
if not written or not PlayerPrefs.Save() then
-- Obtén el motivo antes de restaurar valores, pues eso vuelve a usar la API
-- de guardado. Mantén la puerta como está para permitir reintentar con Save.
local reason = SaveData.GetLastError()
restore_preferences(previous)
status("SAVE FAILED - RETRY SAVE: " .. reason)
return false
end
status(message)
-- Confirma el éxito con el Audio Source de Door Trigger, donde vive el script.
-- El siguiente capítulo lo agrega; no necesitas otro objeto para el audio.
AudioSource.Play(hostId)
return true
end

local function teleport(position)
if not Transform.SetPosition(playerId, position.x, position.y, position.z) then
status("LOAD FAILED: INVALID PLAYER POSITION")
return false
end
-- Evita conservar la velocidad de una caída o salto al recuperar la posición.
CharacterController.SetVelocityY(playerId, 0.0)
return true
end

local function load_checkpoint()
if not valid_targets() then return false end
local previous = capture_preferences()
-- Lee el último punto guardado en el dispositivo, no los cambios sin guardar.
-- PlayerPrefs y SaveData comparten el mismo almacenamiento de datos.
if not SaveData.Reload() then
local reason = SaveData.GetLastError()
-- Conserva los valores anteriores si no se puede leer el dispositivo;
-- aún no se ha movido al jugador ni cambiado la puerta.
restore_preferences(previous)
status("LOAD FAILED: " .. reason)
return false
end
-- Exige las cuatro claves antes de restaurar algo. La primera partida o un
-- Reset exitoso no tienen punto de control; deja la escena como está.
for _, name in ipairs(fields) do
if not PlayerPrefs.HasKey(key(name)) then
status("NO CHECKPOINT")
return false
end
end
-- Usa un valor fuera de rango si una coordenada no se puede leer, en lugar
-- de sustituirla silenciosamente por una posición válida en el origen.
local position = {
x = PlayerPrefs.GetFloat(key("x"), 1e30),
y = PlayerPrefs.GetFloat(key("y"), 1e30),
z = PlayerPrefs.GetFloat(key("z"), 1e30)
}
if not teleport(position) then return false end
-- Restaura la puerta con el mismo punto de control que la posición del jugador.
if not set_door(PlayerPrefs.GetInt(key("door_open"), 0) == 1) then return false end
status("CHECKPOINT LOADED")
return true
end

-- Enlaza los objetos al iniciar este comportamiento, no en cada cuadro.
-- UI.Find busca elementos de UI; GameObject.Find busca GameObjects de la escena.
function Start(gameObjectId)
statusId = UI.Find(statusTextName)
playerId = GameObject.Find(playerName)
doorId = GameObject.Find(doorName)
if not valid_targets() then return end
-- Conserva la posición inicial en Room A para Reset. Aplica el estado guardado
-- de la puerta; Square recuperará la posición guardada cuando el jugador lo pida.
startPosition = Transform.GetPosition(playerId)
set_door(PlayerPrefs.GetInt(key("door_open"), 0) == 1)
-- Selecciona un botón inicial para que el usuario del mando active Save con Cross.
local saveButton = UI.Find("Save Button")
if saveButton >= 0 then Selectable.Select(saveButton) end
status(doorOpen and "DOOR OPEN - SQUARE LOADS" or "WALK TO THE DOOR")
end

-- Carga una vez al presionar Square, no en cada cuadro mientras se mantiene
-- presionado. Leer el dispositivo repetidamente interrumpiría el juego.
function Update(gameObjectId, dt)
if Input.GetButtonDown(Input.SQUARE) then load_checkpoint() end
end

function OnTriggerEnter(gameObjectId, otherGameObjectId)
-- Solo el jugador activa la puerta. Volver a entrar cuando ya está abierta
-- no debe sobrescribir el punto de control una y otra vez.
if otherGameObjectId ~= playerId or doorOpen then return end
if not valid_targets() or not set_door(true) then return end
-- Abre el paso antes de guardar: la falta de tarjeta no debe bloquear Room B.
-- Save permite reintentar con la puerta abierta y la posición actual del jugador.
save_checkpoint(gameObjectId, "DOOR OPEN - CHECKPOINT SAVED")
end

-- Save Button llama aquí mediante Target Entity y Function. gameObjectId
-- identifica a Door Trigger, no al botón de UI que envió el evento.
function saveNow(gameObjectId, elementId, elementName, eventName)
save_checkpoint(gameObjectId, "CHECKPOINT SAVED")
end

-- Reset Button borra solo el progreso del tutorial. Conserva el estado de la
-- escena hasta confirmar que ese reinicio se haya guardado.
function resetProgress(gameObjectId, elementId, elementName, eventName)
if not valid_targets() or startPosition == nil then return end
local previous = capture_preferences()
clear_preferences()
if not PlayerPrefs.Save() then
-- Obtén el error antes de restaurar los valores para explicar por qué falló Reset.
local reason = SaveData.GetLastError()
restore_preferences(previous)
status("RESET FAILED: " .. reason)
return
end
-- Regresa a Room A antes de cerrar la puerta para no dejar atrapado al jugador.
if not teleport(startPosition) then return end
if not set_door(false) then return end
status("PROGRESS RESET")
end

Conecta las propiedades del script​

Regresa a Polygon Engine y selecciona Door Trigger. El componente Lua Behaviour lee las declaraciones de nivel superior anteriores a la primera función y las muestra en Script Properties.

Configura lo siguiente:

Propiedad del scriptValor
Player NamePlayer
Door NameDoor
Status Text NameStatus Text
Save Prefixtwo_room_relay

El script resuelve estos nombres exactos y únicos una sola vez en Start y guarda sus ID en caché. Mantén ambos objetos activados en la escena creada; Start aplica el estado guardado de la puerta después de enlazar las referencias. Si falta un nombre, se informa un error de configuración en lugar de desactivar el acertijo silenciosamente. Un asterisco en el Inspector indica que el GameObject sobrescribe el valor predeterminado de Lua.

Qué hace el script​

  • Start restaura el estado guardado de la puerta.
  • OnTriggerEnter comprueba que el GameObject que entra sea el jugador, abre la puerta, guarda el punto de control y escribe los datos en el dispositivo.
  • Update vuelve a leer explícitamente el documento persistente antes de cargar al presionar Square.
  • Si falla el guardado, conserva las preferencias anteriores del tutorial en RAM, mantiene abierta la puerta durante la partida y permite reintentar con Save. Si falla el reinicio, el estado actual de la puerta y del jugador no cambia. Un reinicio exitoso devuelve al jugador a Room A.
  • saveNow y resetProgress son callbacks de los botones creados en la interfaz.
  • AudioSource.Play(hostId) usará el Audio Source del trigger después de agregarlo en el siguiente capítulo.

Punto de revisión 3​

Presiona Ctrl+S y entra en Play Mode. Camina hacia la puerta. Debe desaparecer y permitirte entrar a Room B. La Console no debe informar errores de script faltante, configuración ni trigger. El primer guardado puede fallar si no hay un dispositivo de guardado con permiso de escritura; la puerta se abre de todos modos y el siguiente capítulo agrega mensajes visibles.

El texto de estado y el audio todavía no existen; sus respuestas visibles y sonoras se agregan en el siguiente capítulo: Agrega interfaz, audio y guardado.