Skip to main content

Lua Gameplay on a Fixed-Memory PS1 Runtime

Creator of Polygon Engine

Lua is valuable in a PS1 engine because it separates gameplay iteration from C runtime work. Lua is also dangerous on a PS1 if it grows without limits.

Polygon Engine's scripting model is built around that tension: scripts should be easy to attach, inspect, and test, while the runtime keeps memory, script count, and API surface bounded.

What Lua Owns

In Polygon Engine, Lua is responsible for gameplay behavior:

  • responding to triggers and collisions,
  • driving pickups, gates, terminals, and stations,
  • updating UI text and focus,
  • calling scene loads,
  • playing audio sources,
  • toggling GameObjects,
  • reading and writing small save data values.

Lua does not own the renderer, asset loader, SPU upload path, mesh format, or build pipeline. Those stay in C/C++.

That split is deliberate. The PS1 runtime should expose a small set of stable APIs instead of letting scripts reach into every subsystem.

GameObject-Attached Scripts

Scripts are attached to scene GameObjects through a Lua Behaviour component. The runtime invokes lifecycle functions when they exist:

function Start(gameObjectId)
Log.Print(GameObject.GetName(gameObjectId) .. " ready")
end

function Update(gameObjectId, deltaTime)
Transform.RotateY(gameObjectId, 45 * deltaTime)
end

function OnTriggerEnter(gameObjectId, otherGameObjectId)
Log.Print("triggered by " .. tostring(otherGameObjectId))
end

The engine also supports script-to-script style calls through GameObject.Invoke and UI Button callback targets. Build/editor validation can detect missing callback functions before the runtime is launched.

Runtime Scripting Limits

The current scripting limits are intentionally visible:

LimitValue
Max script components32
Max unique script sources32
Max properties per script component8
Max script property name32 bytes
Max script property value64 bytes
Max Lua heap512 KB
Max individual script size64 KB

The default project can choose smaller settings, but it cannot exceed the runtime cap. The build validates those caps.

Script Properties

Script properties are one of the most useful workflow features. They let the same Lua file behave differently per GameObject.

For example, a station script can expose:

stationId = 3
promptText = "Activate"
targetGameObjectId = -1

The editor reads defaults, lets the user override values in the Inspector, and stages those values with the scene. The runtime passes them to the script component.

This is the kind of feature that makes a PS1 editor practical: fewer duplicated scripts, more data-driven GameObject behavior.

Instruction Budget

Runaway scripts are especially harmful on a fixed runtime. A bad loop can steal the entire frame and make the game look like a renderer problem.

Polygon Engine exposes an instruction budget setting. When enabled, Lua calls are guarded so runaway scripts can be stopped instead of silently destroying frame time.

The build also warns when the instruction hook interval is larger than the per-call budget, because that configuration lets scripts overshoot before being interrupted.

Authored UI, Not Just Debug Text

The runtime supports up to eight named UI canvases with independent reference sizes, visibility, and sort order. The fixed scene-wide element budget covers:

  • Panel,
  • Text,
  • Image,
  • Button,
  • ProgressBar.

Lua can find UI elements, change text, progress, position, size, and color, toggle element or canvas visibility, change canvas order, and set or read focus:

local progressText = -1
local hudCanvas = -1

function Start(gameObjectId)
hudCanvas = Canvas.Find("HUD")
Canvas.SetEnabled(hudCanvas, true)
progressText = UI.Find("Progress Text")
Text.SetText(progressText, "Stations 0/5")
end

function Update(gameObjectId, deltaTime)
if Input.GetButtonDown(Input.CROSS) then
Text.SetText(progressText, "Activated")
end
end

This is different from drawing temporary debug text. The authored UI is part of the scene data and is staged for the runtime.

Save Data

The SaveData API is a bounded key-value layer backed by one 8,192-byte memory-card block:

if SaveData.Write("checkpoint", "hub") and
SaveData.Write("station_03_complete", "1") then
if not SaveData.Flush() then
Log.Print(SaveData.GetLastError())
end
end

local checkpoint = SaveData.Read("checkpoint")

The runtime supports 32 entries, with 31-character keys and 63-character values. The file includes a standard memory-card header, project-defined title, 16-color icon, versioned payload, and checksum.

The API reports card absence, capacity errors, invalid values, incomplete writes, corrupt payloads, and unsupported versions. Gameplay code can therefore show a real save result instead of assuming that flush() succeeded.

Supported older save payloads are decoded and rewritten in the current format after the next successful flush. Migration stays inside the persistence layer; gameplay scripts continue to read the same keys.

The system is intentionally small. It covers checkpoints, settings, progress flags, and compact player state without turning the memory card into a general database.

Why This Approach Fits PS1

The PS1 does not have room for an unbounded scripting environment. The engine has to make Lua feel friendly while keeping it measurable.

The rule is:

Lua owns game rules.
C runtime owns hardware.
Editor owns validation.
Build owns packaging.

That separation is what makes Lua practical for PS1 homebrew instead of just impressive in a screenshot.

Continue Reading

Saving and testing your game

Use PlayerPrefs for typed progress and settings. Call PlayerPrefs.Save() to persist changes and SaveData.Reload() to load the last save. Check each result before displaying a success message. Keep keys short: PlayerPrefs allows 28 bytes per key and 61 bytes per string value.

Test save, load, reset, and retry with View > Save Data Simulator, then repeat those flows in your packaged game. See the Persistence reference and Two-Room Relay tutorial.

For script errors, Debug builds retain source line information. Keep Lua memory and instruction limits in mind and measure gameplay performance; see the Runtime Reference.

Updated for Polygon Engine 0.1.0-ea.2.