Roblox runtime guide
Why AI-written Luau passes review but breaks in Studio
A code review can prove that syntax is valid and the control flow looks plausible. It cannot prove that a call returns during play, that the right player wins a race, or that cleanup happens after the work is finished.
Evidence note: the chase example below was diagnosed from code shared in a public Roblox developer thread. It was not run through a live Baseplate playtest, so this page does not claim a verified PASS.
The bug that looks reasonable
An NPC controller checks every player and calls a BindableFunction to move toward each target:
while entity.Parent do
for _, player in Players:GetPlayers() do
local character = player.Character
if character and character:GetAttribute("Hidden") then
lerpTo:Invoke(primary, workspace.Start)
elseif character then
lerpTo:Invoke(primary, character.HumanoidRootPart)
end
end
task.wait(0.1)
end
It reads like a continuous chase. In a running server it can freeze on the first destination, choose the last player in the list, and fail to notice that a target hid mid-movement.
Failure 1: Invoke() is a hidden wait
BindableFunction:Invoke() does not fire and continue. It waits until the connected OnInvoke function returns. If LerpTo.OnInvoke only returns after movement finishes, the outer loop cannot re-check targets during that movement.
Fast proof: print a timestamp immediately before and after the call. If the second line appears only when the NPC reaches its destination, the target-selection loop is blocked.
print("before Invoke", os.clock())
lerpTo:Invoke(primary, target)
print("after Invoke", os.clock())
Failure 2: looping over players does not choose a player
Calling movement once for every player means several destinations compete in one update. In practice, the last qualifying entry returned by Players:GetPlayers() often becomes the apparent winner.
Selection should be a separate step: filter for a living character, reject Hidden, enforce the 200-stud radius, then select exactly one nearest root part.
local function nearestVisibleRoot(origin, radius)
local winner, winnerDistance = nil, radius
for _, player in Players:GetPlayers() do
local character = player.Character
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
local root = character and character:FindFirstChild("HumanoidRootPart")
if humanoid and humanoid.Health > 0 and root
and not character:GetAttribute("Hidden") then
local distance = (root.Position - origin).Magnitude
if distance < winnerDistance then
winner, winnerDistance = root, distance
end
end
end
return winner
end
Failure 3: cleanup shares the critical path
A spawned chase loop does not pause its caller. An unconditional entity:Destroy() after task.spawn() can run immediately and create misleading warnings such as an infinite yield on a child that belonged to the destroyed entity.
Keep lifetime management independent from target selection and movement:
task.delay(180, function()
if entity.Parent then
entity:Destroy()
end
end)
Failure 4: the controller and movement job disagree
Fixing the outer loop is not enough if the movement function still owns a stale destination until arrival. The durable shape is four separate responsibilities:
- Observe: sample players and their live attributes.
- Select: publish one desired target for this update.
- Move: read the desired target repeatedly and support cancellation or retargeting.
- Clean up: stop connections and destroy the entity on a separate lifetime condition.
The exact movement change belongs inside LerpTo.OnInvoke. Do not work around a blocking movement function by spawning a new Invoke() every 0.1 seconds; that creates overlapping tweens or movement jobs instead of true retargeting.
Studio verification checklist
- Test with two players on opposite sides of the NPC.
- Hide the current target while the NPC is moving, not after it arrives.
- Kill or remove the target and confirm another player is selected.
- Move every valid player outside 200 studs and confirm the NPC returns to spawn.
- Watch Output for overlapping movement, infinite yields, and access to destroyed instances.
- Run the scenario twice; stale connections often appear only on the second spawn.
- Only call it fixed after the final run is clean and behavior matches the assertions.
Copyable mission prompt
Playtest this NPC chase controller in Roblox Studio.
Use two players. While the NPC is moving, toggle Hidden on the current target,
kill that target, and move all surviving players outside 200 studs.
Verify that it retargets without waiting for the old destination, returns to spawn
when nobody qualifies, creates only one movement job, and leaves Output clean.
Do not report PASS until every assertion succeeds in a fresh rerun.
Turn the checklist into a real Studio loop
Baseplate's free core flow connects your agent to Roblox Studio, runs the playtest, reads the real Output, and requires a clean rerun before PASS.
Run the free verification loop