Integrating Lua for Gameplay Scripting
I wanted users to program their games without modifying or rebuilding the engine's code. A door, for example, needs to respond to input, remember its state, play a sound, and update a message. Lua gave me a way to let users connect those actions within the project.
Integrating it meant deciding which work belonged in scripts and which systems should remain in native code. Quick experimentation mattered, but I also needed to control memory use and execution time on PS1. That division shapes much of Polygon Engine's API.
Dividing work between scripts and native systems
For a door, the script can decide whether the player has permission to open it, choose its next state, request movement, and update the interface. Those decisions are closely tied to the rules of this particular game.
Calculations repeated across large sets of data, such as vertex transformations or collision-geometry queries, are better handled by native systems with structures designed for that work. The API makes those systems available from Lua. A script can request an action without reimplementing calculations the engine already performs.
A native call can still involve substantial work. If a function performs a broad search and a script calls it every update, that cost adds up. When reviewing a behavior, I need to consider both the frequency of its calls and what each function does.
The Lua manual explains the language's embedding model. Polygon Engine supplies the game-facing API and decides which capabilities are available in that environment. A script written for another Lua application does not automatically have the same libraries or engine functions here.
The script lifecycle
To organize behaviors, I used callbacks that will be familiar to Unity developers. Awake handles initial setup, Start runs before the first update, and Update handles per-frame behavior. FixedUpdate uses the engine's bounded 30 Hz simulation step.
That fixed step defines how simulation advances, but it does not guarantee thirty rendered frames per second under every workload. If the machine falls behind, trying to recover every pending update can make matters worse. The engine therefore limits catch-up work.
I also try to update each system only when needed. Opening a door should trigger its sound once; a label only needs updating when its message changes. Repeating those operations every frame uses resources and can produce audio or UI problems that are difficult to trace back to the responsible script.
The scripting overview documents callbacks and exposed properties. This structure lets me separate initialization, updates, and interaction events. When returning to a script, it is easier to locate its initial state and the functions that modify it during play.
Controlling memory use and execution
Memory management needs attention too. Each temporary table requires storage that Lua's garbage collector must eventually reclaim. If a script rebuilds tables or produces diagnostic strings every update, those allocations repeat throughout the game and can affect performance.
Where a behavior allows it, I prefer preparing its data during initialization and reusing it during play. Some functions need to create new values, so this is not a prohibition on allocation. I look at whether that work really needs to happen so frequently.
Polygon Engine sets a limit on Lua's heap. If a script reaches it, the error should help identify the behavior using too much memory. As well as containing the problem, the diagnostic needs to provide enough information to correct it in the project.
The engine also limits instructions to contain non-terminating Lua loops or excessive execution. That protection does not replace profiling: instruction counts do not directly represent elapsed time, and native calls can perform additional work. Those aspects need separate checks.
Preparing Lua bytecode for PS1
Building the game includes preparing Lua bytecode for PS1. This avoids compiling source on the console and lets the release runtime omit the full parser. The result still runs in the Lua virtual machine; it is not converted to native MIPS instructions.
The bytecode must be generated with the Lua version and numeric configuration expected by the runtime. A file compiled with an arbitrary desktop Lua installation may not be compatible. That is why bytecode preparation belongs to the engine's build process.
Numeric configuration also affects performance. The PS1 CPU has no hardware floating-point unit, although it can perform those operations in software. They are possible, but their cost matters when repeated frequently. Geometry systems can use fixed point and handle conversions when receiving data through the scripting API.
Connecting scripts to a playable interaction
The two-room tutorial provides a small example for reviewing this integration. It connects input, a trigger, a moving door, sound, interface messages, and saved progress. Each part has a specific role, so the interaction can be followed from player input to the change on screen.
Reviewing a script like this helps me identify which API functions make interaction easier and where unnecessary work remains. If a common operation requires a lot of repeated code, it is worth considering what the engine should handle. If a call costs too much, I need to reduce its frequency or improve the system implementing it.
Integrating Lua brought me closer to the workflow I wanted: change an interaction's rules in the project and try them quickly. To keep that convenient on PS1, the API and native systems need to handle repeated work efficiently while scripts focus on each game's behavior.