Skip to main content

Common Gameplay Patterns

These recipes combine general-purpose Components and Lua APIs. They are not genre modules: adapt the data, names, and rules to the game you are building.

Trigger Pickup With Persistent State

Author a visible GameObject with a trigger Collider and Lua Behaviour. Give the player a stable Player tag.

pickupKey = "pickup.keycard"

function Start(gameObjectId)
if PlayerPrefs.GetInt(pickupKey, 0) == 1 then
GameObject.SetActive(gameObjectId, false)
end
end

function OnTriggerEnter(gameObjectId, otherGameObjectId)
if not GameObject.CompareTag(otherGameObjectId, "Player") then
return
end

local previous = PlayerPrefs.GetInt(pickupKey, 0)
if not PlayerPrefs.SetInt(pickupKey, 1) or not PlayerPrefs.Save() then
local reason = SaveData.GetLastError()
PlayerPrefs.SetInt(pickupKey, previous)
Log.Print("Pickup save failed: " .. reason)
return
end
AudioSource.PlayOneShot(gameObjectId)
GameObject.SetActive(gameObjectId, false)
end

This recipe leaves the pickup available after a save failure. Leave and re-enter its trigger to retry. Give it an Audio Source for the one-shot effect.

Raycast Interaction

Use Physics.Raycast when the project needs a custom interaction flow instead of the built-in Interaction Ray and Interactable pairing.

cameraGameObjectId = -1
interactionDistance = 2.25

function TryInteract(gameObjectId)
local origin = Transform.GetPosition(cameraGameObjectId)
local rotation = Transform.GetRotation(cameraGameObjectId)
if origin == nil or rotation == nil then return end

local yaw = math.rad(rotation.y)
local pitch = math.rad(rotation.x)
local dx = math.sin(yaw) * math.cos(pitch)
local dy = -math.sin(pitch)
local dz = math.cos(yaw) * math.cos(pitch)

local hit, targetGameObjectId = Physics.Raycast(
origin.x, origin.y, origin.z,
dx, dy, dz,
interactionDistance,
nil,
false
)
if hit then
GameObject.Invoke(targetGameObjectId, "Interact", gameObjectId)
end
end

For most first-person projects, the native Components are simpler and provide prompt range, line-of-sight, input binding, and cooldown without script polling.

Locked Gate

Separate persistent state from presentation. The same function can drive a Collider, Portal, and transform animation.

requiredItemKey = "pickup.keycard"
portalGameObjectId = -1

function Interact(gameObjectId, sourceGameObjectId)
if PlayerPrefs.GetInt(requiredItemKey, 0) ~= 1 then
return
end

Collider.SetIsTrigger(gameObjectId, true)
Portal.SetOpen(portalGameObjectId, true)
TransformAnimator.Play(gameObjectId, "Open")
Interactable.SetEnabled(gameObjectId, false)
end

Canvas Menu

Canvas visibility and controller focus are separate responsibilities. Always select a valid Button after opening a menu.

menuCanvasName = "Pause Menu"
resumeButtonName = "Resume Button"

local menuCanvasId = -1
local resumeButtonId = -1

function Start(gameObjectId)
menuCanvasId = Canvas.Find(menuCanvasName)
resumeButtonId = UI.Find(resumeButtonName)
end

function SetMenuOpen(open)
if Canvas.SetEnabled(menuCanvasId, open) and open then
Selectable.Select(resumeButtonId)
end
end

UI Button callbacks receive the target GameObject ID, element ID, element name, and "click". Configure the exact custom function name in the Button Inspector.

Scene Transition Guard

Prevent repeated input or Button submission from queuing the transition again:

targetScene = "Scenes/hub.json"
local loading = false

function OpenScene(gameObjectId)
if loading then return end
loading = true
AudioSource.Play(gameObjectId)
SceneManager.LoadScene(targetScene)
end

The target scene must be enabled in Build Settings.

Typed Settings

function ApplyAudioSettings(gameObjectId)
Music.SetVolume(PlayerPrefs.GetFloat("musicVolume", 0.8))
end

function SetMusicVolume(gameObjectId, value)
if PlayerPrefs.SetFloat("musicVolume", value) then
if not PlayerPrefs.Save() then
Log.Print("Volume preference not persisted: " .. SaveData.GetLastError())
end
Music.SetVolume(value)
end
end

Use raw SaveData only when string-level storage is intentional. PlayerPrefs and SaveData share the same fixed Memory Card store.

Debugging Checklist

  • confirm every GameObject owns the Component required by the API;
  • cache and validate GameObject, Canvas, and UI IDs;
  • match custom callback names exactly;
  • include every SceneManager.LoadScene target in Build Settings;
  • handle failed audio, navigation, persistence, and controller capabilities;
  • run Validate Project, PS1 Budget, Production Acceptance, and the packaged CUE.