How Should I Make A Good Stun System?

Hi guys. So, I’ve been working on my fighting game for a long time, and I want to ask you what is the best way for scripts to know if a player can use abilities or not. All this time I have been inserting “Stun” bool value into the character and “eStun” bool value into the enemy character for a period of time, and in each ability script I have been reducing the speed of the enemy character, so that they could not walk away from the combo, after that there’s this piece of code:

eHum.WalkSpeed = 0 -- Making enemy character not walking
eHum.JumpPower = 0
spawn(function()
    wait(1)
    if not eChar:FindFirstChild("Stun") and not eChar:FindFirstChild("eStun") then -- Checking if the enemy is stunned
       eHum.WalkSpeed = 16
       eHum.JumpPower = 50
    end
end)

The issue is that I’m not sure if this is a good way to make a stun system. I have to insert this to every single ability script, and it doesn’t always work. Any ideas would be highly appreciated. Thanks in advance.

3 Likes

Well, why don’t you check when there’s the “Stun” and “eStun” instances inside of the character itself and changing the speed accordingly instead of doing it directly from the ability script? You could use ChildAdded and ChildRemoved in a local script.

Character.ChildAdded:Connect(function(Child)
   if Child.Name == "Stun" or Child.Name == "eStun" then
       Character.Humanoid.WalkSpeed = 0;
       Character.Humanoid.JumpPower = 0;
   end;
end);
Character.ChildRemoved:Connect(function(Child)
   if Character:FindFirstChild("Stun") == nil and Character:FindFirstChild("eStun") == nil then
       Character.Humanoid.WalkSpeed = 16;
       Character.Humanoid.JumpPower = 50;
   end;
end);
1 Like

Thank you for an idea. I have two question. Since it’s a local script, can exploiters do something with it so they can avoid getting slowed? And how will it influence game, may it be laggy for players because the script will trigger every time something is added or removed?

Yes, they definitely can. They can just set their WalkSpeed and JumpPower back to normal, OR remove the stun instances. And no, if you’re wondering, switching to a server script won’t change anything. A solution I use too is to keep checking if there’s a stun through RunService.RenderStepped so, even if they change their speed and JumpPower back they will be set back to “stunned” (since it checks every frame). You also need to protect the instances with an AncestryChanged event, to prevent exploiters removing their stun instances.

Depends, if your game doesn’t add or remove a LOT of instances from the character then it shouldn’t be a problem. If you prefer using ChildAdded/Removed and are worried about lag, I recommend creating a folder named “Stuns” and put stuns in there + check when something is added in there instead of the character.

I hope I helped you, if you have any other questions let me know.

you could ragdoll them while they’re stunned so they can’t move.
also, I recommend using attributes instead as you don’t have to keep Instancing a new “stun” value.

Attributes wouldn’t work. If there’s a new stun while there’s another stun, the new one wouldn’t be able of adding up as the old stun would remove the attribute so the new stun wouldn’t have effect, unless you have another way to do it.

1 Like

I’d really appreciate it if you explain those things. I have an idea, making a server script, which will detect if a player joins and do all the things you listed in the first reply (Character.ChildAdded, Character.ChildRemoved), though I’m not sure what will cause less lag.

Sure.

The RenderStepped event fires every frame, which means if we use it to check if there’s a stun, every frame that a stun is found the player’s speed and jumpPower will be set back to 0, so even if an exploiter changes them back they will be set back to 0 as the event would keep firing. It would work like this:

local RunService = game:GetService("RunService");
RunService.RenderStepped:Connect(function)
   if Character:FindFirstChild("Stun") or Character:FindFirstChild("eStun") then
      Character.Humanoid.WalkSpeed = 0;
      Character.Humanoid.JumpPower = 0;
   else
      Character.Humanoid.WalkSpeed = 16;
      Character.Humanoid.JumpPower = 50;
   end;
end);

so, the process would be (if there was an exploiter):
Player receives a stun;
RenderStepped fires, which sets the player’s WalkSpeed and JumpPower to 0;
Exploiter instantly sets back the WalkSpeed to 16 and JumpPower to 50;
RenderStepped fires again immediately after, finding “Stun” inside the character and setting again the WalkSpeed and JumpPower to 0;

However, the exploiter could do this too:

Character.ChildAdded:Connect(function(Child)
   if Child.Name == "Stun" or Child.Name == "eStun" then
      wait() -- I know wait() is now deprecated but (ignorant) exploiters mostly use deprecated methods
      Child:Destroy();
   end;
end);

This right here would just destroy the instance used to detect the stun, which would lead to the if statement not finding any stun aka setting WalkSpeed and JumpPower to 16 and 50. Luckily, we have an event that can save us, AncestryChanged. When creating a stun instance, we can connect this event to make sure the instance isn’t destroyed before needed.

local function CreateStun(Time, eChar)
   Time = Time or 0.5;
   local Removed = false;
   local StunInstance = Instance.new("BoolValue");
   StunInstance.Name = "Stun";
   StunInstance.Parent = eChar;
   StunInstance.AncestryChanged:Connect(function()
      if StunInstance.Parent ~= eChar and Removed == false and game.Players:GetPlayerFromCharacter(eChar) then -- makes sure that the time before removing the stun hasn't passed and the enemy character is actually a player
          game.Players:GetPlayerFromCharacter(eChar):Kick("Exploit");
      end;
   end;
   task.wait(Time);
   Removed = true;
   StunInstance:Destroy();
end;

-- PS, this can be done on the server so it cannot be exploited!

This is a function i’d personally make to create a stun instance, as it’s easy to use and it has an anti-exploit too.

EDIT: Notice that the server check for when the client destroys the stun instance does not work anymore because of the recent changes, so this method is not really reliable now. See this for more info:

6 Likes
local Players = game:GetService("Players")

Players.PlayerAdded:Connect(function(plr)
    local stun = Instance.new("BoolValue", plr.Character)
    stun.Name = "Stunned"

    stun:GetPropertyChangedSignal("Value"):Connect(function()
        if stun.Value then
            -- Stun character
        else
            -- Destun character
        end
    end)
end)

Put this in a script in server script service, it’s fully tamper-free.
The way you stun a character is by changing the “Value” of the Stun boolean value in a character to true or false.

workspace.Felliouss.Stunned.Value = true
wait(10)
workspace.Felliouss.Stunned.Value = false

Thank you so much for such a detailed anwser. I’ve tested the code and something seems to be wrong. If I put the CreateStun function into the ability script, the script starts to wait after the enemy character got hit by the ability, because of task.wait(), so should I make a bindable event for this and fire it in another server script? And there’s another problem, the function doesn’t slow down the enemy, it makes them stuck, so the problem is, there are different abilities in my game and some of them ragdoll, some of them make the enemy stuck, some of them just slow down. I’m not sure if it’s possible to solve this problem, but if it, please let me know.

Oh right, sorry I completely forgot. The way you can fix that is by using coroutines. Just wrap the function or just use a coroutine directly into the function.

local function CreateStun(Time, eChar)
   coroutine.wrap(function()
      Time = Time or 0.5;
      local Removed = false;
      local StunInstance = Instance.new("BoolValue");
      StunInstance.Name = "Stun";
      StunInstance.Parent = eChar;
      StunInstance.AncestryChanged:Connect(function()
         if StunInstance.Parent ~= eChar and Removed == false and game.Players:GetPlayerFromCharacter(eChar) then -- makes sure that the time before removing the stun hasn't passed and the enemy character is actually a player
             game.Players:GetPlayerFromCharacter(eChar):Kick("Exploit");
         end;
      end;
      task.wait(Time);
      Removed = true;
      StunInstance:Destroy();
   end)();
end;

I got a little problem. Output says: missing argument #1 to ‘wrap’ (function expected) . My code is:

coroutine.wrap(CreateStun(.5,eChar))

I don’t remember how wrapping actual functions with coroutines work, but if I remember correctly you should do coroutine.wrap(CreateStun)(.5,eChar); instead. Not sure tho.

2 Likes

Thank you so much. If you have some free time, you can also check my other topics.

1 Like

Sure, I’ll check them out once I’m free.