Hello guys, is there any way to make an optimized system?
I’m building a +1 Speed Escape Obby, and I’m wondering if there’s a better way to avoid using a while loop for XP gain.
Basically, I want to give XP only when the player is running and when Activity == nil (because if the attribute exists, it means the player is on a treadmill and should instead get XP passively while AFK).
Is there a more efficient or event-based way to handle this instead of constantly checking inside a loop?
function LevelServiceServer:SetupCharacter(player: Player, character: Model)
local humanoid: Humanoid = character:WaitForChild("Humanoid")
self.ApplySpeedModifier(player)
local isRunning = false
local alive = true
humanoid.Running:Connect(function(speed)
isRunning = speed > 0
end)
humanoid.Died:Connect(function()
alive = false
end)
character.AncestryChanged:Connect(function(_, parent)
if not parent then
alive = false
end
end)
task.spawn(function()
while alive do
local activity = player:GetAttribute("Activity")
if isRunning and humanoid.Health > 0 and not activity then
print("working")
self.AddXp(player, xpPerSecond, true)
elseif activity then
print("not working")
end
task.wait(1)
end
end)
end
Use the stuff you already have. humanoid.Running fires when the player starts or stops moving, and GetAttributeChangedSignal("Activity") fires when the treadmill activity changes. Have both call one evaluate() function.
For the ticker (instead of using loops), use something like task.delay() and have it schedule itself every second. Before giving XP, do one quick check to make sure the conditions are still valid. If not, stop and do not schedule another tick.
function LevelServiceServer:SetupCharacter(player: Player, character: Model)
local humanoid: Humanoid = character:WaitForChild("Humanoid")
self.ApplySpeedModifier(player)
local isRunning = false
local isDead = false
local xpThread = nil -- handle to the pending task.delay()
-- re-queue itself every second while conditions are true
local function scheduleXp()
if xpThread then return end
xpThread = task.delay(1, function()
xpThread = nil
if isDead or not isRunning or player:GetAttribute("Activity") then return end
self.AddXp(player, xpPerSecond, true)
scheduleXp()
end)
end
-- cancel the pending tick if one alraedy exists
local function stopXp()
if xpThread then
task.cancel(xpThread)
xpThread = nil
end
end
-- this gets called by every event, single place that decides if the ticker runs
local function evaluate()
if isDead then stopXp(); return end
if isRunning and not player:GetAttribute("Activity") then
scheduleXp()
else
stopXp()
end
end
humanoid.Running:Connect(function(speed)
isRunning = speed > 0
evaluate()
end)
humanoid.Died:Connect(function()
isDead = true
evaluate()
end)
character.AncestryChanged:Connect(function(_, parent)
if not parent then
isDead = true
evaluate()
end
end)
player:GetAttributeChangedSignal("Activity"):Connect(evaluate)
end
Fixed script:
function LevelServiceServer:SetupCharacter(player: Player, character: Model)
local humanoid: Humanoid = character:WaitForChild(“Humanoid”)
self.ApplySpeedModifier(player)
local isRunning = false
local xpThread = nil
local function stopXp()
if xpThread then
task.cancel(xpThread)
xpThread = nil
end
end
local function startXp()
if xpThread then return end -- already running
xpThread = task.spawn(function()
while true do
task.wait(1)
-- re-check inside the tick in case state changed mid-wait
if not isRunning or humanoid.Health <= 0 or player:GetAttribute("Activity") then
stopXp()
return
end
self.AddXp(player, xpPerSecond, true)
end
end)
end
local function evaluateState()
local activity = player:GetAttribute("Activity")
if isRunning and humanoid.Health > 0 and not activity then
startXp()
else
stopXp()
end
end
humanoid.Running:Connect(function(speed)
isRunning = speed > 0
evaluateState()
end)
player:GetAttributeChangedSignal("Activity"):Connect(evaluateState)
humanoid.Died:Connect(function()
isRunning = false
stopXp()
end)
character.AncestryChanged:Connect(function(_, parent)
if not parent then
isRunning = false
stopXp()
end
end)
end
evaluateState() is called by every relevant event instead of checked every second. The XP thread only spawns when the player is actually running with no Activity, and gets cancelled the moment any condition breaks.
task.cancel() cleanly kills the thread — no alive flag needed, no waiting for the next loop iteration to notice the state changed.
startXp() guard — if xpThread then return end prevents double-spawning if two events fire in the same frame (e.g. speed changes while Activity also clears).
Re-check inside the tick — the task.wait(1) means up to 1 second can pass before the thread notices a mid-wait state change. The guard at the top of the tick catches that edge case and exits cleanly rather than granting a phantom XP tick.
GetAttributeChangedSignal(“Activity”) replaces polling the attribute every second, so treadmill transitions are instant instead of delayed by up to 1 second.