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:
- 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.
-
Sometimes this error message appears when testplaying:
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. - 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:
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!