3. Create the Door Trigger and Lua
The door will open when the player crosses an invisible volume. One Lua script will own the trigger, checkpoint position, persistent door state, UI callbacks, and reset path.
Create the Trigger GameObject
- Select Scene > Create Entity > Create Empty and keep it at the scene root,
separate from
DoorandPlayer. Disabling Door must not disable its controller. - Rename the GameObject
Door Trigger. - Set its Transform Position to
(0, 1.0, -2.25). - In the Inspector, select Add Component > Collider.
- Set Type to AABB and Size to
(1.6, 2.0, 1.0). HoldCtrland click a numeric field to type an exact value, then press Enter. - Enable Is Trigger.
The trigger sits immediately before the door when approaching from Room A. It must overlap the player capsule but must not block movement.

The finished Door Trigger sits on the Room A side of the doorway. The Inspector shows the script name bindings, checkpoint sound and trigger dimensions. Audio is added in the next chapter.
Open the image to view it at full size.Create and Attach the Script
- With
Door Triggerselected, choose Add Component > Lua Behaviour. - The Select Lua Script dialog opens. Select Create New. If you already added an empty Lua Behaviour, its Create Script... button opens the same creation workflow.
- Name it
tutorial_controller.lua, select the Behaviour template, and place it inAssets/Scripts, then select Create. - Select Open Script.
- Replace the generated contents with the code below and save the file.
You can also download the
complete script
and copy it over the generated file at Assets/Scripts/tutorial_controller.lua.
Return to the engine and confirm that the Lua Behaviour shows the script as PS1 Ready.
-- Exposed Inspector properties. Keep these names unique in the scene.
playerName = "Player"
doorName = "Door"
statusTextName = "Status Text"
savePrefix = "two_room_relay"
local playerId = -1
local doorId = -1
local statusId = -1
local startPosition = nil
local doorOpen = false
local fields = { "x", "y", "z", "door_open" }
local function key(name)
return savePrefix .. "." .. name
end
local function status(message)
Log.Print(message)
if statusId >= 0 then
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
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
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
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
local function clear_preferences()
for _, name in ipairs(fields) do
-- An absent key returns false; deleting it is already complete.
PlayerPrefs.DeleteKey(key(name))
end
end
local function restore_preferences(previous)
-- Release newly inserted keys first, including a partially written set.
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
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)
if not written or not PlayerPrefs.Save() then
local reason = SaveData.GetLastError()
restore_preferences(previous)
status("SAVE FAILED - RETRY SAVE: " .. reason)
return false
end
status(message)
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
CharacterController.SetVelocityY(playerId, 0.0)
return true
end
local function load_checkpoint()
if not valid_targets() then return false end
local previous = capture_preferences()
if not SaveData.Reload() then
local reason = SaveData.GetLastError()
-- Keep this checkpoint available when loading fails.
restore_preferences(previous)
status("LOAD FAILED: " .. reason)
return false
end
for _, name in ipairs(fields) do
if not PlayerPrefs.HasKey(key(name)) then
status("NO CHECKPOINT")
return false
end
end
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
if not set_door(PlayerPrefs.GetInt(key("door_open"), 0) == 1) then return false end
status("CHECKPOINT LOADED")
return true
end
function Start(gameObjectId)
statusId = UI.Find(statusTextName)
playerId = GameObject.Find(playerName)
doorId = GameObject.Find(doorName)
if not valid_targets() then return end
startPosition = Transform.GetPosition(playerId)
set_door(PlayerPrefs.GetInt(key("door_open"), 0) == 1)
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
function Update(gameObjectId, dt)
if Input.GetButtonDown(Input.SQUARE) then load_checkpoint() end
end
function OnTriggerEnter(gameObjectId, otherGameObjectId)
if otherGameObjectId ~= playerId or doorOpen then return end
if not valid_targets() or not set_door(true) then return end
-- An unavailable card never blocks walking into Room B.
-- Save retries persist this live door state and the current player position.
save_checkpoint(gameObjectId, "DOOR OPEN - CHECKPOINT SAVED")
end
function saveNow(gameObjectId, elementId, elementName, eventName)
save_checkpoint(gameObjectId, "CHECKPOINT SAVED")
end
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
local reason = SaveData.GetLastError()
restore_preferences(previous)
status("RESET FAILED: " .. reason)
return
end
-- Return to Room A before closing the door, so reset cannot trap the player.
if not teleport(startPosition) then return end
if not set_door(false) then return end
status("PROGRESS RESET")
end
Connect the Script Properties
Return to Polygon Engine and select Door Trigger. The Lua Behaviour component reads
the top-level declarations before the first function and exposes them under
Script Properties.
Set:
| Script property | Value |
|---|---|
| Player Name | Player |
| Door Name | Door |
| Status Text Name | Status Text |
| Save Prefix | two_room_relay |
The script resolves these exact, unique names once in Start and caches their
IDs. Keep both objects enabled in the authored scene; Start applies the saved
door state after binding the references.
A missing name produces a setup error instead of silently disabling the puzzle.
An asterisk in the Inspector means the GameObject overrides the Lua default.
What the Script Does
Startrestores the saved door state.OnTriggerEnterchecks that the entering GameObject is the player, opens the door, saves the checkpoint, and flushes it.Updateexplicitly reloads the persisted document before loading on Square.- A failed save preserves the previous tutorial preferences in RAM, keeps the live door open, and offers retry through Save. A failed reset keeps the live door/player state unchanged. A successful reset returns the player to Room A.
saveNowandresetProgressare callbacks for authored UI buttons.AudioSource.Play(hostId)will use the trigger's Audio Source after the next chapter adds it.
Checkpoint 3
Press Ctrl+S and enter Play Mode. Walk toward the door. The door should
disappear and allow entry into Room B. The Console must not report a missing
script, setup or trigger error. The initial save can fail if no writable save device
is available; the door still opens, and the next chapter adds visible feedback.
The status text and audio do not exist yet, so their visible feedback comes in the next chapter: Add UI, Audio, and Saving.