Server-Sided Stamina system

I’m trying to make a server-sided stamina system, for security purposes. I could just make it client-sided and call it a day, but I’d rather be safe from potential exploiters. I’m pretty sure there is a more efficient (and prettier) way to set this up but I have no clue how. Any idea is welcome!
The way the system works is simple: when the player double-presses W and holds it the second time, the humanoid.WalkSpeed is multiplied by X amount (1.85 in the script), and stamina is consumed. When the player releases the W key, their WalkSpeed is set to the previous one and the stamina starts recharging.

Unfortunately, I’m having multiple issues:

  1. The stamina bar doesn’t update properly. No clue why, but it either doesn’t move or starts “jumping” at random lenghts whenever stamina is consumed or recharged.
  2. Sometimes this error message appears when testplaying: image
    I suspect it’s because the script runs BEFORE the character and the stamina values are loaded, which I tried to fix by putting a wait() but didn’t really change much.
  3. For some reason, after releasing the W key, the WalkSpeed is set to 0, which is odd considering the previous WalkSpeed value is saved in a table and then used to restore it once the player stops holding W or their stamina hits 0.

Here is the GUI setup:
image

Here are the scripts, with extra explanations as comments in the code:

LocalScript (inside StarterCharacterScripts)
local Players = game:GetService("Players")
local UIS = game:GetService("UserInputService")
local RS = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local plr = Players.LocalPlayer
local char = plr.Character or plr.CharacterAdded:Wait()
local hum = char:WaitForChild("Humanoid")

local Stamina = char:GetAttribute("Stamina")

local Bar = script.Parent
local Remote = RS:WaitForChild("Remotes"):FindFirstChild("RemoteEvent")

local tapSpeed = 0.35
local lastTap = tick()
local isSprinting = false

local function isHolding()
	return UIS:IsKeyDown(Enum.KeyCode.W)
end

UIS.InputBegan:Connect(function(input,isTyping)

	if isTyping or Stamina == 0 then return end

	if input.KeyCode == Enum.KeyCode.W then
		if tick() - lastTap < tapSpeed and not isSprinting then

			if isHolding() then
				isSprinting = true
				Remote:FireServer("Started")
			end

		end
		lastTap = tick()
	end

end)

UIS.InputEnded:Connect(function(input)
	if input.KeyCode == Enum.KeyCode.W then
		if not isHolding() and isSprinting then
			isSprinting = false
			Remote:FireServer("Ended")
		end
	end
end)

--I used attributes because I thought they were a bit more practical
char:GetAttributeChangedSignal("Stamina"):Connect(function() 
	local stamina = char:GetAttribute("Stamina")
	local maxStamina = plr:GetAttribute("MaxStamina")
	local bar = plr.PlayerGui:WaitForChild("PlayerInfo").Stamina.Ba
	bar:TweenSize(UDim2.new((1/100) * stamina, 0, 1 ,0), Enum.EasingDirection.InOut, Enum.EasingStyle.Linear, 0.25)
end)
ServerScript
local RS = game:GetService("ReplicatedStorage")
local Remote = RS:WaitForChild("Remotes"):FindFirstChild("RemoteEvent")
local RunService = game:GetService("RunService")

local speedModifier = 1.85
local staminaRegen = 10
local sprintCost = 5
local sprintingPlayers = {} --I saw another Youtuber using this method where each player gets recoded inside a table with its previous WalkSpeed when they start sprinting and thought it was a pretty good method.

--As I said before, I wanted to use Attributes instead of Values. Everytime the player respawns, their Stamina (and mana) are set to the Max amount attributed inside the Player Instance. Might this be causing problems?
game.Players.PlayerAdded:Connect(function(plr)
	plr:SetAttribute("MaxStamina",100)
	plr:SetAttribute("MaxMana",300)

	plr.CharacterAdded:Connect(function(char)
		char:SetAttribute("Mana",plr:GetAttribute("MaxMana"))
		char:SetAttribute("Stamina",plr:GetAttribute("MaxStamina"))
	end)
end)

Remote.OnServerEvent:Connect(function(plr,state)
	local char = plr.Character or plr.CharacterAdded:Wait()
	local hum = char:WaitForChild("Humanoid")
	local Stamina = char:GetAttribute("Stamina")
	
	if state == "Started"  and  hum.MoveDirection.Magnitude > 0 then
		sprintingPlayers[plr.Name] = hum.WalkSpeed
		hum.WalkSpeed = hum.WalkSpeed * speedModifier	
	elseif state == "Ended" then
		hum.WalkSpeed =	sprintingPlayers[plr.Name]
		sprintingPlayers[plr.Name] = nil
	end
end)


wait(1) --Tried putting a wait here to prevent the error mentioned before but doesn't look like it's doing much.

--I initially thought about using while wait() do but I thought RunService.Heartbeat would be more optimized. I'm still not 100% sure about having a function constantly running on the server because I'm afraid it might cause lag, so if someone has a better option, let me know!
RunService.Heartbeat:Connect(function()
	for index, player in pairs(game:GetService("Players"):GetPlayers()) do
		local char = player.Character or player.CharacterAdded:Wait()
		local hum = char:WaitForChild("Humanoid")
		
		local maxStamina = player:GetAttribute("MaxStamina")
		local stamina = char:GetAttribute("Stamina")
		local name = player.Name

--WARNING! Spaghetti code here. I'm pretty sure there is something messed up which I'm completely missing.
		if not sprintingPlayers[name] then
			if stamina > maxStamina then
				char:SetAttribute("Stamina",maxStamina)
			elseif stamina < maxStamina then
				char:SetAttribute("Stamina",char:GetAttribute("Stamina") + staminaRegen)
			end
		else
			if stamina >= sprintCost then
				char:SetAttribute("Stamina",char:GetAttribute("Stamina") - sprintCost)
			else
				player.Character.Humanoid.WalkSpeed = sprintingPlayers[name]
				sprintingPlayers[name] = nil
			end
		end
	end
end)

Thanks in advance! :slight_smile:

3 Likes

The client has full control over the position of their character no matter what
Doing this server sided is unnecessary

I’m aware, but I’m trying to prevent exploiters from changing the Stamina value.

It would be much easier to just set your walkspeed to 30 as an exploiter than look into a complex stamina system

while wait() do 
    workspace.PapaBreadd.WalkSpeed = 30
end

is all it takes to break that

2 Likes

Okay? I know that lol and it’s not what I’m asking.

Its a waste of your time and other peoples time to try and secure something which simply cannot be secured, and was never designed to be secured

I’m not trying to sound rude, but can you elaborate on that?
What I’m trying to secure is the amount of Stamina, so that exploiters can’t change the amount and potentially have an infinite stamina. As I said before, I’m aware that the client has full control over their character, but I’ve seen other threads talking about methods to also prevent the client from changing the WalkSpeed to abnormal values.
Is it really a waste of time?

As @PapaBreadd said, doing this server-side is unnecessary, I tried this before, and the client receiving stamina data from the server turned out pretty choppy. Making the client-side choppy and the UI choppy.

I as well recommend doing this client-side, you could possibly secure the amount of Stamina client-side by either sending the client stamina data to the server constantly for it to check it up (not really recommended), or either check the stamina amount client-side (recommended but bypassable).

Either way, exploiter have full control over their character, the client can replicate its character to the server, meaning there is full client control over the character. Maybe you can make it so you can maybe check on the server from time to time on how long the Player has been running, if you calculate the maximum time of player being able to run, (Example: 30 seconds) you could maybe somehow detect if the player has been running for more that 30 seconds, and then kick them. (However, this might come with some problems, example: A player runs for 28 seconds (not exploiting the stamina), and then the player lags for over 2-3 seconds, on their client, they will see that they don’t have any more stamina, but on the server it still shows that the player is running, causing a false positive in the anti cheat system.)

You could possibly try to make it less rewarding for the exploiters to exploit the stamina values (what do I mean by this is you that you shouldn’t make stamina such a big deal in your game, so the exploiters don’t really have to exploit it in the first place…), try making a “Stamina Potion” in your game which would grant Infinite stamina for – seconds… That would somewhat make the exploiters and the players on a equal level.

At the end of the day, just do it Client-Side, exploiter have full control over their character, so it is just a waste of time, you can maybe put a little anti-cheat for speed hacks, anti gravity hacks and other stuff, but doing this for the stamina system isn’t the best idea. (In my opinion)

4 Likes

Thank you so much for elaborating! I appreciate it and actually understood why it’s better to handle it client-side.
I just finished editing my script, to make it client-sided. While working on it, however, I thought about the system and wondered if there was a way to make the server, somehow, aware of the Stamina amount, without actually having it handle the system, leaving the client in charge of it to prevent lag. (I need this for future features of the project I’m working on)
I came up with a fairly simple method that fires a remote to the server with current amount of stamina everytime the player starts running, stops or the amount hits 0\max amount. I think it works pretty well, despite being asynchronous.
Here is the code, if anyone is interested (I’d also appreciate any feedback or suggestion on how to improve it because sometimes the Remote seems to fire when I just press W once without holding it)

LocalScript
local Players = game:GetService("Players")
local UIS = game:GetService("UserInputService")
local RS = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local plr = Players.LocalPlayer
local char = plr.Character or plr.CharacterAdded:Wait()
local hum = char:WaitForChild("Humanoid")

local Stamina = char:GetAttribute("Stamina")

local Bar = script.Parent
local Remote = RS:WaitForChild("Remotes"):FindFirstChild("RemoteEvent")

local tapSpeed = 0.35
local lastTap = tick()
local isSprinting = false
local speedModifier = 2.1
local staminaTime = 1
local originalSpeed = hum.WalkSpeed

local function isHolding()
	return UIS:IsKeyDown(Enum.KeyCode.W)
end

local function getStamina()
	Remote:FireServer(char:GetAttribute("Stamina"))
	print("Client Stamina:"..char:GetAttribute("Stamina"))
end

UIS.InputBegan:Connect(function(input,isTyping)

	if isTyping or Stamina == 0 then return end

	if input.KeyCode == Enum.KeyCode.W then
		if tick() - lastTap < tapSpeed and not isSprinting then

			if isHolding() then
				isSprinting = true
				hum.WalkSpeed = hum.WalkSpeed * speedModifier
				spawn(getStamina)
				
				while isSprinting do
					wait(staminaTime)
					if char:GetAttribute("Stamina") > 0 then
						char:SetAttribute("Stamina",char:GetAttribute("Stamina")-5)
					else
						hum.WalkSpeed = hum.WalkSpeed / speedModifier
						spawn(getStamina)
					end
				end
			end

		end
		lastTap = tick()
	end

end)

UIS.InputEnded:Connect(function(input)
	if input.KeyCode == Enum.KeyCode.W then
		if not isHolding() and isSprinting then
			isSprinting = false
			hum.WalkSpeed = originalSpeed
			spawn(getStamina)
			while not isSprinting do
				wait(staminaTime)
				if char:GetAttribute("Stamina") < plr:GetAttribute("MaxStamina") then
					char:SetAttribute("Stamina",char:GetAttribute("Stamina")+10)
				end
			end
		end
	end
end)



char:GetAttributeChangedSignal("Stamina"):Connect(function()
	local stamina = char:GetAttribute("Stamina")
	local maxStamina = plr:GetAttribute("MaxStamina")
	local bar = plr.PlayerGui:WaitForChild("PlayerInfo").Stamina.Bar
	
	if stamina < 0 then
		char:SetAttribute("Stamina",0)
		spawn(getStamina)
		
	end
	
	if stamina > maxStamina then
		char:SetAttribute("Stamina",maxStamina)
		spawn(getStamina)
	end
	
	if stamina == maxStamina then
		hum.WalkSpeed = originalSpeed
		spawn(getStamina)
	end

	bar:TweenSize(UDim2.new((1/100) * stamina, 0, 1 ,0), Enum.EasingDirection.Out, Enum.EasingStyle.Linear, staminaTime)

end)


ServerScript
local RS = game:GetService("ReplicatedStorage")
local Remote = RS:WaitForChild("Remotes"):FindFirstChild("RemoteEvent")
local RunService = game:GetService("RunService")


game.Players.PlayerAdded:Connect(function(plr)
	plr:SetAttribute("MaxStamina",100)
	plr:SetAttribute("MaxMana",300)

	plr.CharacterAdded:Connect(function(char)
		char:SetAttribute("Mana",plr:GetAttribute("MaxMana"))
		char:SetAttribute("Stamina",plr:GetAttribute("MaxStamina"))
		char:SetAttribute("EstimatedStamina",char:GetAttribute("Stamina"))
	end)
end)


Remote.OnServerEvent:Connect(function(plr,newStaminaAmount)
	print("Server Stamina:"..newStaminaAmount)
	plr.Character:SetAttribute("EstimatedStamina",newStaminaAmount)

end)
2 Likes

The easiest way to check if your system is secure is to check your parameters, you sure you trust this newStaminaAmount from the client?

Anyway for something like managing a continuous rate of resources asynchronously this should be a leaky bucket method which is what FPS games do for their ammo count and fire rate.

A visualization of this is this anti cheat by ForbiddenJ as described below, and hey its even uncopylocked.

For your case with a sprint system the maximum heat gauge should be higher to adjust for the additional speed the player might possibly get.

If you go with this method the best punishment should be teleporting the player back to the previous CFrame or the maximum distance they could have moved during the time span in order to avoid worse case scenario punishing false positives like what FJ does.

Yea, I suppose exploiters could fire the remote and pass any amount the want, right? I didn’t consider it but I’m not too worried because I just wanted to make the server aware of the amount of stamina. I can try adding some if statements to mantain the amount between 0 and maxAmount perhaps. Any suggestion?

Oh, this sounds interesting. Could you tell me more about it\link some resources?

This looks interesting, I’m looking into it right now, thank you!

A way that might solve your problem is to use a number value to store the stamina and have a while loop on the server that checks if the max stamina is more than the set value for every player. The script has to use a for loop to go through all the players. In order for this to not be laggy, I recommend checking like every 10 seconds. However the exploiters might measure the time of 10 seconds and create a while loop for themself that resets the stamina to the huge value every 10 seconds. In order to fix this, you might need to set the waiting in the while loop to a random value from 10 to 15 seconds) This might still be bypassed, but it’s a good solution for now or at least the best one I can think of. Sorry for the late reply.

Or at the end of the day just obfuscate your script. Might not be the best solution but It makes exploiters struggle a lot.

1 Like