Making a stamina system

local Stamina = 100

local Sprinting = false

local function sprint(active)
	if active then
		Humanoid.WalkSpeed = Humanoid.WalkSpeed + 6
	else
		Humanoid.WalkSpeed = Humanoid.WalkSpeed - 6
	end
	
	Sprinting = active
end

UserInputService.InputBegan:Connect(function(input)
    if input.KeyCode ~= Enum.KeyCode.LeftShift or input.KeyCode == Enum.KeyCode.ButtonR2 then return end
		
	sprint(true)
end)

UserInputService.InputEnded:Connect(function(input)
    if input.KeyCode ~= Enum.KeyCode.LeftShift or input.KeyCode == Enum.KeyCode.ButtonR2 then return end
	
	sprint(false)
end)

RunService.RenderStepped:Connect(function()
	if Sprinting then
		if Stamina > 0 then
			Stamina = Stamina - 1
			wait(1)
		else
			sprint(false)
		end
	end
end)

Here’s what I have so far. The problems I am currently facing are
The stamina drains way too quickly. I have a wait(1), but that does literally nothing?? And once stamina == 0 and I set sprint to false, it continuously the lowers the players speed. Please note I cannot do

Humanoid.WalkSpeed = 22 -- or any specific number

I do ±6 as the speed has to be relative to the players speed, as there are different classes a player can have, each with their own speed

14 Likes

Maybe you need a stamina debounce so it doesn’t drain practically instantly? So something like:

if stamina > 0 and staminadebounce == false then
staminadebounce = true
stamina = stamina -1
wait(1)
staminadebounce = false
else

Sorry I’m on mobile so my formatting is bad

Edit: Also I think your issue with the walkspeed continuously decreasing is because nothing is preventing sprint(false) from continuously firing once stamina equals zero. So it continuously subtracts 6 walkspeed on the renderstepped. I think you need a debounce here as well.

3 Likes

Using RenderStepped runs a new instance of whatever code you’re running in there every frame, so the wait will not wait for it. Try reducing the value of stamina. Also I think Heartbeat works fine for this scenario as well.

local Stamina = 100

local Sprinting = false
local Exhausted = false
local speedDiff = 6

local function sprint(active) -- not the best example
	if Exhausted then return end
	if active then
		Humanoid.WalkSpeed = Humanoid.WalkSpeed + speedDiff
	else
		Humanoid.WalkSpeed = Humanoid.WalkSpeed - speedDiff
	end
	if Stamina <= 0 then 
		Exhausted = true
	end
	Sprinting = active
end

UserInputService.InputBegan:Connect(function(input)
    if input.KeyCode ~= Enum.KeyCode.LeftShift or input.KeyCode == Enum.KeyCode.ButtonR2 then return end
	sprint(true)
end)

UserInputService.InputEnded:Connect(function(input)
    if input.KeyCode ~= Enum.KeyCode.LeftShift or input.KeyCode == Enum.KeyCode.ButtonR2 then return end
	sprint(false)
end)

RunService.RenderStepped:Connect(function()
	if Sprinting and not Exhausted then
		if Stamina > 0 then
			Stamina = Stamina - 0.1
		else
			sprint(false)
		end
	end
end)
4 Likes

I have a couple of issues with your code, specifically this part:

RunService.RenderStepped:Connect(function()
    if Sprinting then
        if Stamina > 0 then
            Stamina = Stamina - 1 wait(1)
        else
            sprint(false)
        end
    end
end)

To answer your immediate question, I’d like to point out that you’re creating a new thread every render frame. This means every render frame Stamina = Stamina - 1 is being executed. This, also, means that it’s removing stamina every frame, then pausing that specific thread for 1 second. I’ll actually get to fixing your code after my next point:

Another problem arises when using RenderStepped: It can vary. It can vary a lot. One person’s RenderStepped may fire, say, once every 1/25th of a second, while someone on their beast of a pc gets a solid 1/60th. This means that the lower the FPS, the less stamina they lose per second. Uh oh!

Oh man how do i fix this!?

It’s easy. RenderStepped passes an argument, dt, or deltaTime or “the change in time.” This value is equal to the amount of seconds that have passed since the previous render frame. Let’s see this helpful little variable in action!

RunService.RenderStepped:Connect(function(dt)
    if Sprinting then
        if Stamina > 0 then
            Stamina = Stamina - 1*dt
        else
            sprint(false)
        end
    end
end)

When we multiply our constant, (in this case 1), by our delta, (the change in time), we get the magic number to subtract! Meaning you will lose exactly 1 stamina per second, regardless of how often RenderStepped fires. Hallelujah!

9 Likes

Or just use Stepped so that you aren’t freezing frames to run code. RenderStepped realistically should only be used for render loops, like changing the CFrame of the camera.

5 Likes

There’s no way to replenish it tho? Once it hits 0 I can’t get it to go back up

Heres my butchered attempt. It’s kinda close, only problem is once it hits 0, it does back up, but my players speed will keep being lowered until I can’t move

local Sprinting = false
local Exhausted = false
local SpeedDiff = 6

local function sprint(active)
	if Exhausted then return end
	
	if active then
		Humanoid.WalkSpeed = Humanoid.WalkSpeed + SpeedDiff
	else
		Humanoid.WalkSpeed = Humanoid.WalkSpeed - SpeedDiff
	end

	Sprinting = active
end

UserInputService.InputBegan:Connect(function(input)
    if input.KeyCode ~= Enum.KeyCode.LeftShift or input.KeyCode == Enum.KeyCode.ButtonR2 then return end

	sprint(true)
end)

UserInputService.InputEnded:Connect(function(input)
    if input.KeyCode ~= Enum.KeyCode.LeftShift or input.KeyCode == Enum.KeyCode.ButtonR2 then return end

	sprint(false)
end)

RunService.Heartbeat:Connect(function()
	if Sprinting then
		if Stamina > 0 then
			Stamina = Stamina - 0.5
		else
			sprint(false)
			Exhausted = true
		end
	else
		if Stamina < 100 then
			Stamina = Stamina + 0.5
		end
	end
	
	if Exhausted and Stamina > 0 then
		Exhausted = false
	end
	
	local RoundedNumber = math.floor(Stamina + 0.5)
	PlayerGui.HUD.Game.StaminaBar.Stamina.Text = RoundedNumber
	PlayerGui.HUD.Game.StaminaBar.Bar:TweenSize(UDim2.new(RoundedNumber / 100, 0, 1, 0), 'Out', 'Linear', 0.05, true)
end)
4 Likes

Why aren’t you using delta time?

This:

while true do
    local w = wait()
    stam.Value = stam.Value + w*10
end
4 Likes

Ah, I would’ve included a regenerate stamina but you didn’t have it in your original post, thought it was a one time thing.

local Stamina = 100

local SprintHeld = false
local Sprinting = false
local Exhausted = false

local RunRefresh = 20 -- when to allow running after exhausted
local SpeedDiff = 6
local DrainRate = 20 -- drain per second

local function sprint(active)
	if Exhausted then return end -- we can't run because we're exhausted!
	if active then
		Humanoid.WalkSpeed = Humanoid.WalkSpeed + SpeedDiff
	else
		Humanoid.WalkSpeed = Humanoid.WalkSpeed - SpeedDiff
	end
	Sprinting = active
end

UserInputService.InputBegan:Connect(function(input)
    if input.KeyCode ~= Enum.KeyCode.LeftShift or input.KeyCode == Enum.KeyCode.ButtonR2 then return end
	SprintHeld = true
	sprint(SprintHeld)
end)

UserInputService.InputEnded:Connect(function(input)
    if input.KeyCode ~= Enum.KeyCode.LeftShift or input.KeyCode == Enum.KeyCode.ButtonR2 then return end
	SprintHeld = false
	sprint(SprintHeld)
end)

RunService.Heartbeat:Connect(function(DeltaTime)
	if Sprinting then
		if Stamina > 0 then
			Stamina = Stamina - DrainRate * DeltaTime
		else
			sprint(false)
			Exhausted = true
		end
	elseif Stamina < 100 then
		Stamina = Stamina + DrainRate * DeltaTime
		if Stamina > RunRefresh then -- we can now run again!
			Exhausted = false
			if SprintHeld then -- resume running because player is still holding down the sprint key!
				sprint(SprintHeld)
			end
		end
	end
end)
28 Likes

Absoulete genius! :smiley: Thank you so much!! :smiley: :smiley:

^ Can I just note, is it better to update a UI element from this, or fire a BindableEvent inside the Heartbeat function to update the UI from StarterGui

5 Likes

I mean, isn’t a BindableEvent exploitable though?
If it were to me, I’d modify the UI itself via LocalPlayer.PlayerGui

1 Like

Anything on the client can be exploited.

This is just as exploitable

3 Likes

so how do we make it not exploitable

you can’t , everything can be changed on cilent. Just make sure that the stamina value isn’t outside the local script

Small bump, again, but for future references: you can’t nor are you supposed to. If the exploiter modifies and changes their own UI, that’s kinda… to their own detriment as long as they aren’t changing the speed value itself (which, if it were me I’d handle that on the server-side rather than client-side), I don’t see why you’d worry about “oh no, an exploiter hacked their own UI and now it doesn’t work anymore!” Reason I’m telling you to not worry nor care too much about it is because, speaking from experience, you can’t save everything from exploiters, and trying to do so is burning through energy and wasting time. So, while I’d make the speed change server-side, don’t think too much in depth about everything purely visually related :smile:.

Cheers