Hi I’m currently making my own PvP Combat game and I was wondering if anyone knows how to have the players stay stunned while still being hit multiple times by another player.
I’m using a module script (That is Server-Sided) to perform this task. This is currently what I have written:
local StunModule = {}
function StunModule:Stun(Character:Instance, Duration: number, StunWalkSpeed: number, StunJumpPower: number)
local Humanoid = Character:WaitForChild("Humanoid")
if Humanoid then
if Humanoid.Health <= 0 then
return
end
else
return
end
local WalkSpeed = Humanoid.WalkSpeed
local JumpPower = Humanoid.JumpPower
task.spawn(function()
Humanoid:SetAttribute("Stunned", true)
Humanoid.WalkSpeed = StunWalkSpeed
Humanoid.JumpPower = StunJumpPower
wait(Duration) --// This is the issue.
Humanoid:SetAttribute("Stunned", false)
Humanoid.WalkSpeed = WalkSpeed
Humanoid.JumpPower = JumpPower
end)
end
return StunModule
The stun works, but the issue is that after the player has been stunned once, the wait function will override the next time a player gets stunned again causing the player to be not stunned and get out of a combo.
How do I make it so that the after the player gets stunned again, the task.spawn function restarts from the beginning and makes the player stunned completely, while also ignoring the wait function?
repeat task.wait(1) Duration -= 1 until Duration <= 0
That way you can just add to the duration, causing a “stacking” effect. You would of course, need to store the duration somewhere else and add to it as a different variable.
Yes, you could use an attribute to keep track of the duration, you just need to accommodate the code to utilize it. You could utilize the :GetAttributeChangedSignal() function too, if you really wanted to. In that case, you could just check if the time is greater than 0 and set their WalkSpeed accordingly.
I had this problem some time ago and found the solution
local StunModule = {}
local StunTimes = {}
local function SetDefaultHumanoidAttributes(Character : Instance)
local Humanoid = Character:FindFirstChild("Humanoid")
Humanoid:SetAttribute("WalkSpeed", Humanoid.WalkSpeed)
Humanoid:SetAttribute("JumpPower", Humanoid.JumpPower)
end
function StunModule:Stun(Character : Model, Duration: number, StunWalkSpeed: number, StunJumpPower: number)
local Humanoid = Character:FindFirstChild("Humanoid")
if not Humanoid or Humanoid.Health <= 0 then return end
local WalkSpeed = Humanoid:GetAttribute("WalkSpeed")
local JumpPower = Humanoid:GetAttribute("JumpPower")
if not WalkSpeed or not JumpPower then
SetDefaultHumanoidAttributes(Character)
WalkSpeed = Humanoid:GetAttribute("WalkSpeed")
JumpPower = Humanoid:GetAttribute("JumpPower")
end
StunTimes[Character] = os.clock()
Humanoid:SetAttribute("Stunned", true)
Humanoid.WalkSpeed = StunWalkSpeed
Humanoid.JumpPower = StunJumpPower
task.delay(Duration, function()
if os.clock() - StunTimes[Character] >= Duration then
Humanoid:SetAttribute("Stunned", false)
Humanoid.WalkSpeed = WalkSpeed
Humanoid.JumpPower = JumpPower
end
end)
end
return StunModule
So the problem is that after waiting, we don’t know if the target was stunned again. To do this, you can use newproxy(), which generates a random but unique value each time it is called. It’s not that much more efficient than math.random() or tick(), but it’s the best option.
There’s a lot of limitations to having a loop to check for stun, such as stun having to be a multiple of how often the stun loop checks and task.wait()/wait() being inneffective at very low delays.
Anyways, here’s the code using newproxy()
StunModule.StunProxy = newproxy() --//Also, you should use a variable in the module instead of an attribute.
function bestunned()
local CurrentProxy = newproxy() --//Declare new proxy.
StunModule.StunProxy = CurrentProxy --//If this function is called again, the module's StunProxy variable will change.
task.spawn(function()
Humanoid:SetAttribute("Stunned", true)
Humanoid.WalkSpeed = StunWalkSpeed
Humanoid.JumpPower = StunJumpPower
task.wait(Duration) --// Use task.wait() instead of wait.
if Module.StunProxy == CurrentProxy then
Humanoid:SetAttribute("Stunned", false)
Humanoid.WalkSpeed = WalkSpeed
Humanoid.JumpPower = JumpPower
end
end)
end
Oh, and compared to AkaNub’s code, this is closer to what you’d see in a fighting game where new stun takes priority over old stun. If someone stunned for 10s is hit at 5 seconds for 1 second of stun, they’ll recover in 1 second, not 5.
I had a similar problem with disabling movement/jumping, so I ended up using integer attributes instead of booleans to prevent the issue of the player being able to move when they shouldn’t.
I also used attributes for offsets and multipliers, to make sure nothing weird happens when increasing/decreasing walkspeed.
Your method seems interesting, however. It might be worthwhile for me to look into using newproxy() if I run into a similar issue for future projects.
It could also potentially work for an attribute system similar to Minecraft’s, where stat modifiers are given unique ids.
Ok I tried using this, but sometimes the player gets stun locked permanently except if you hit them again. How do you fix this and should the script be a local or server script?
Oh, I assumed that you had one of these module scripts per character, not one module for everyone. You should use a dictionary to store who’s CurrentProxy is who’s.
Is it recommended to have these module scripts per character or one module for everyone? Also how do I use a dictionary to store who’s CurrentProxy is who’s? I still don’t know how to use meta tables…
In my honest opinion, there is no need to complicate things if you just want something simple. Here is a script that will run every hearbeat that will do the calculations and applying for you.
--!strict
local Players = game:GetService("Players")
local StarterPlayer = game:GetService("StarterPlayer")
local RunService = game:GetService("RunService")
-- stun settings
local STUN_DURATION_ATTRIBUTE_NAME: string = "StunDuration"
local NORMAL_WALK_SPEED: number = StarterPlayer.CharacterWalkSpeed
local STUN_WALK_SPEED: number = NORMAL_WALK_SPEED / 4
RunService.Heartbeat:Connect(function(deltaTime: number): ()
-- loop through every player every heartbeat
for _: number, player: Player in Players:GetPlayers() do
-- check if the player has a character, a humanoid, and a valid attribute
local humanoid: Humanoid? = if player.Character then player.Character:FindFirstChildOfClass("Humanoid") else nil
local hasAttribute: boolean = if humanoid ~= nil then typeof(humanoid:GetAttribute(STUN_DURATION_ATTRIBUTE_NAME)) == "number" else false
if humanoid ~= nil and hasAttribute == true then
-- calculate the new duration by subtracting the current duration by deltaTime
local newDuration: number = math.clamp(humanoid:GetAttribute(STUN_DURATION_ATTRIBUTE_NAME) - deltaTime, 0, math.huge)
-- apply the new duration and walkspeed
humanoid:SetAttribute(STUN_DURATION_ATTRIBUTE_NAME, newDuration)
humanoid.WalkSpeed = if newDuration > 0 then STUN_WALK_SPEED else NORMAL_WALK_SPEED
end
end
end)
You could do one modulescript per character, or one modulescript for all characters. One global modulescript is more preformant, but also harder to make and debug.
How a global modulescript would work is by storing everyone’s stats, such as health and stun, as a dictionary in a dictionary. Then when using a function related to a characters stats, we grab that entry from the dictionary.
Like this:
CharacterStats = { --//First dictionary layer
Player1 = { --//Second dictionary layer
Health = 100;
Speed = 16
StunProxy = newproxy()
};
Player2 = { --//Still the second dictionary layer
Health = 100;
Speed = 16
StunProxy = newproxy()
}
}
--[[
If we needed to hit Player1, we'd get their stats with CharacterStats["Player1"]
]]
We’re not using newproxy() for metatables, but as an identifier. You could do the same with a number that increments by 1, or any type of other value as long as it’s guarenteed to be unique.
Besides the unnecessary use of RunService.Heartbeat(), all this does is handle the stun slowing down the player, not actually apply the stun status nor prevent stun ending when it shouldn’t.
local StunModule = {}
function StunModule:Stun(Character:Instance, Duration: number, StunWalkSpeed: number, StunJumpPower: number)
local Humanoid = Character:WaitForChild("Humanoid")
if Humanoid then
if Humanoid.Health <= 0 then
return
end
else
return
end
local WalkSpeed = Humanoid.WalkSpeed
local JumpPower = Humanoid.JumpPower
-- Create a coroutine to manage the stun effect
local stunCoroutine = coroutine.create(function()
Humanoid:SetAttribute("Stunned", true)
Humanoid.WalkSpeed = StunWalkSpeed
Humanoid.JumpPower = StunJumpPower
-- Wait for the stun duration
coroutine.yield(Duration)
Humanoid:SetAttribute("Stunned", false)
Humanoid.WalkSpeed = WalkSpeed
Humanoid.JumpPower = JumpPower
end)
-- Resume the coroutine, which will yield and resume when the stun duration is over
coroutine.resume(stunCoroutine)
end
return StunModule
ReplicatedStorage is accessible to both clients and server, while server script storage is server only. If you don’t need to put something in ReplicatedStorage, it should go in ServerStorage/ServerScriptStorage.