How can I handle threads in my situation?

Hello, I am making a combat game and at the moment I am working on basic combat. I noticed that when someone gets hit consecutively the delay threads that lower the player’s movement get increased back to normal even if they’re getting hit again. I’ll post a video and code to show an example of the problem.

In this video, I’m trying to show that even when my friend is getting punched, his movement speed is quickly reset by the task.delay I use. If there is either an alternate way of using .delay or a thread handler please let me know!

CODE:

--SERVER CODE/LOGIC

Player2:SetAttribute("IsAttacked", true)
Player2:FindFirstChildOfClass("Humanoid"):TakeDamage(ComboData.Damage)
Player2.Humanoid.WalkSpeed = 1
Player2.Humanoid.JumpHeight = 0
		
if Combo == 5 then
	Knockback.ApplyKnockback(Player2.PrimaryPart, Player2.PrimaryPart:FindFirstChild("RootAttachment"), .25, {
				
			VectorVelocity = Player1.PrimaryPart.CFrame.LookVector * 100,
			
			})
		--else
		--	Knockback.ApplyKnockback(Player2, .25, {

		--		Velocity = Player1.PrimaryPart.CFrame.LookVector * 5,
		--		Power = 50

		--	})
end
		
task.delay(.35, function()
	Player1:SetAttribute("IsAttacking", false)
end)
		
task.delay(.9, function()
	Player2:SetAttribute("IsAttacked", false)
	Player2.Humanoid.WalkSpeed = 16
	Player2.Humanoid.JumpHeight = 7.2
end)

i’m not an expert on threads but have you ever considered the use of co-routines?

Even if I add a co-routine, what could I do to stop the threads if a person got hit again?

i really don’t know

character limit, ignore

I may have gone overboard… no, I definitely went overboard, you were just asking about how to cancel the delayed tasks if there was another attack. But I hope this will be useful to you in more than just that. I tried to comment a lot, without stating things that are very obvious.

I haven’t tested anything. Some of it are assumptions, like assuming that you want to trigger an attack when parts touch and one of the attack animations is playing, while not having attacked anyone yet in that animation cycle (because that’s probably what I would do), but what I wanted to do, beyond just solving the issue you were having, is to show you some useful principles you can take away and keep. I’m not gonna list them here, you’ll sense them from the comments.

You could also copy-paste some of the logic to the client, to create effects and whatnot faster, and without having to use remotes in either direction, keeping safe what’s supposed to be safe, while not sacrificing the eye candy to ping.

Feel free to ask any questions.

-- server Script

local Players = game:GetService('Players')

		   -- attacker   victim(s)
local combos: {[Model]: {[Model]: { comboCount: number, lastAttackTime: number }}?} = {}
local comboDamageAddition = 0.1    -- will be multiplied by the combo count
local maxComboCount = 5    -- to cap the multiplier
local attackingDuration, underAttackDuration = 0.35, 0.9

-- most probably not ideal, use body movers if you can, like a short-lived VectorForce or LinearVelocity
local function applyKnockback(Victim: Model, direction: Vector3, magnitude: number)
	-- correct the direction to point slightly upwards, so the floor friction doesn't stop the impulse
	direction = Vector3.new(direction.X, Victim.HumanoidRootPart.Position.Y + 0.5, direction.Z).Unit
	Victim.HumanoidRootPart:ApplyImpulse(direction * magnitude)	
end

local function afterAttacking(Attacker: Model, Victim: Model?)
	if not combos[Attacker] then
		-- the attacker is dead
	elseif os.clock() - combos[Attacker][Victim].lastAttackTime > attackingDuration then
		-- the attacker is not attacking that victim anymore, but might be attacking someone else
		combos[Attacker][Victim] = nil
	end
	if os.clock() - Attacker:GetAttribute('lastAttackTime') > attackingDuration then
		-- the attacker is not attacking anyone anymore
	end
end

local function afterAttacked(Victim: Model, Attacker: Model?)
	if not combos[Victim] then
		-- the victim is dead
	elseif not combos[Attacker] or not combos[Attacker][Victim] then
		--[[	the victim is no longer being attacked by that attacker, but might be under attack from someone else;
				as long as attackingDuration < underAttackDuration this will be ture, but because I'm not sure about
				how the scheduler works exactly, I can't claim that this will be true if the attackingDuration is
				one cycle's worth of time (1/30 of a second on the server I believe) smaller than underAttackDuration;
				if you want to be absolutely sure, and maybe even want to use values larger than underAttackDuration,
				add this check as well: or os.clock() - combos[Attacker][Victim].lastAttackTime > underAttackDuration	]]--
	end
	if os.clock() - Victim:GetAttribute('lastAttackedTime') > underAttackDuration then
		-- the victim is no longer being attacked by anyone
		Victim.Humanoid.WalkSpeed = 16
		Victim.Humanoid.JumpHeight = 7.2
	end
end

local function onAttack(Attacker: Model, Victim: Model, damage: number)
	
	-- makes sure both are still alive, just to be safe
	if not combos[Attacker] or not combos[Victim] then
		return
	end
	
	local timeOfAttack = os.clock()
	Attacker:SetAttribute('lastAttackTime', timeOfAttack)
	Victim:SetAttribute('lastAttackedTime', timeOfAttack)
	
	local comboData = combos[Attacker][Victim] or { comboCount = 0, lastAttackTime = timeOfAttack }
	-- need this value just for knowing when to knockback
	local rawComboCount = comboData.comboCount + 1
	local cappedComboCount = math.min(rawComboCount, maxComboCount)
	Victim.Humanoid:TakeDamage(damage * (1 + cappedComboCount * comboDamageAddition))
	Victim.Humanoid.WalkSpeed = 1
	Victim.Humanoid.JumpHeight = 0
	combos[Attacker][Victim] = comboData
	
	--[[	assuming that you want to apply the knockback when maxComboCount is reached, this is how you will know
			it was just reached - from this point on rawComboCount will be maxComboCount + 1;
			you could even separate the two, have the knockback trigger and the maxComboCount be different things	]]--
	if rawComboCount == maxComboCount then							 -- receiver
		applyKnockback(Victim,
			-- direction of the force															magnitude of the force
			CFrame.lookAt(Attacker.HumanoidRootPart.Position, Victim.HumanoidRootPart.Position).LookVector, 100)
	else
		applyKnockback(Victim,
			CFrame.lookAt(Attacker.HumanoidRootPart.Position, Victim.HumanoidRootPart.Position).LookVector, 35)
	end
	
	task.delay(attackingDuration, afterAttacking, Attacker, Victim)
	task.delay(underAttackDuration, afterAttacked, Victim, Attacker)
	
end

local function onCharacterAdded(Character: Model, playerStats: {[string]: any})
	
	local damageFactors = { Head = 1.5, UpperTorso = 1, LowerTorso = 1.2 }
	--[[	a damage multiplier based on the part that's hit, you could use only HumanoidRootPart
			if you want to keep the hitbox fair/consistent for all, or you could impose avatar scale	]]--
	local partDamage: {[string]: number} = { LeftHand = 5, RightHand = 5, LeftFoot = 7, RightFoot = 7 }
	--[[	how much damage each part will deal; for tools this should be set in Studio by adding
			a "damage" attribute to the tool's damaging part	]]--
	local activeAnimationTracks: {[string]: boolean} = {}
	--[[	that is, those animations that can currently deal damage; they are playing, have played again
			or looped since the last hit, and have not hit any target since		]]--
	
	local function connectAnimator(Animator: Animator)
		--[[	this allows us to monitor animation tracks on the server; it only registers a track when it is
				played for the first time, so it's theoretically possible that damage will not happen on that
				first playing (if the damaging part was already touching the damage-receiving part when the
				animations started), but this would probably be very rare, and is outweighed by the benefits	]]--
		Animator.AnimationPlayed:Connect(function(animationTrack)
			-- e.g. if your animations were named "Attack_LeftHand", "Attack_Sword", etc.
			local damagingPart = string.sub(animationTrack.Animation.Name, 8)
			if not partDamage[damagingPart] then
				--[[	we're not interested in unregistered animations/parts
						but we'll stil won't discard those that are added in later (tools)	]]--
				return
			elseif activeAnimationTracks[damagingPart] == nil then
				-- these will connect only once because we're explicitly checking for nil
				animationTrack.DidLoop:Connect(function()
					-- the part can damage again after its animation loops
					activeAnimationTracks[damagingPart] = true
				end)
				animationTrack.Stopped:Connect(function()
					-- the part can't damage anymore once its animation stops playing
					activeAnimationTracks[damagingPart] = false
				end)		
			end
			-- the part can damage after its animation starts playing
			activeAnimationTracks[damagingPart] = true
		end)
	end
	
	local function processDescendant(Descendant: Instance)
		if Descendant:IsA('Animator') then
			return connectAnimator(Descendant)
		elseif not Descendant:IsA('BasePart') then
			return
		end
		if damageFactors[Descendant.Name] then
			-- an example for if you used % values for damage resistance
			Descendant:SetAttribute('damageFactor',
				damageFactors[Descendant.Name] * (100 - playerStats.damageResistance) / 100)
			--[[	we're not interested in registering taken damage for these parts, as each character 
					will be processing the damage they deal, because that's much simpler when it comes to tracking	]]--
			return
		elseif not partDamage[Descendant.Name] and not Descendant:GetAttribute('damage') then
			-- return if it's neither one of the damaging parts that load in, nor a tool
			return
		end
		-- an example for if you used % values for damage multiplication; could even be <100% if you want to nerf someone
		local damage = (partDamage[Descendant.Name] and partDamage[Descendant.Name] or Descendant:GetAttribute('damage'))
			* playerStats.damageMultiplier
		
		-- only damaging parts are left at this point, and when they are touched we check conditions and deal damage
		Descendant.Touched:Connect(function(OtherPart)
			-- we're only interested if the other part can be damaged, and the attack animation of our part is "active"
			if OtherPart:GetAttribute('damageFactor') and activeAnimationTracks[Descendant.Name] then
				-- this will put the part on cooldown until its attack animation is played or looped again
				activeAnimationTracks[Descendant.Name] = false
													-- our damage adjusted by the victim's damage factor
				onAttack(Character, OtherPart.Parent, damage * OtherPart:GetAttribute('damageFactor'))
			end
		end)
	end
	
	--[[	here, you can just wait for other descendants and add other things that will be necessary for further actions;
			because at this point, the character can still not receive any damage	]]--
	local Humanoid: Humanoid = Character:WaitForChild('Humanoid')
	Character:WaitForChild('HumanoidRootPart')
	Character.HumanoidRootPart:WaitForChild('RootRigAttachment')
	combos[Character] = {}
	Humanoid.Died:Connect(function()
		local victims = combos[Character]
		combos[Character] = nil
		afterAttacking(Character)
		afterAttacked(Character)
		for Victim in victims do
			-- trigger all of the victims' afterAttacked logic
			afterAttacked(Victim, Character)
		end
	end)
	
	--[[	this is both for tools/gear that will be equipped, and limbs or Animator that have not loaded yet
			adjust if you're using a custom tool system, or make sure you parent tools to the Character		]]--
	Character.DescendantAdded:Connect(processDescendant) 
	
	-- go through the already loaded stuff
	for _, Descendant in Character:GetDescendants() do
		processDescendant(Descendant)
	end
	
end

local function onPlayerAdded(Player: Player)
	local playerStats = { damageResistance = 10, damageMultiplier = 105 }    -- get from a DataStore
	Player.OnCharacterAdded:Connect(function(Character) onCharacterAdded(Character, playerStats) end)
	if Player.Character then onCharacterAdded(Player.Character, playerStats) end
end

Players.PlayerAdded:Connect(onPlayerAdded)
-- this is really only necessary for Studio testing
for _, Player in Players:GetPlayers() do task.spawn(onPlayerAdded, Player) end
2 Likes

You didn’t have to make a whole entire script :sob:, I’ll read it rn

Coroutines are really jank at the moment, and it is recommended to use the task library over them whenever possible.

While this is really good, the problem with it is the delay. If multiple attacks are happening consecutively then it is prone to have the player speed reset due to the delay

I know, I haven’t slept for 24 hours so I’m in crazy mode. I could’ve just told you “you have to track the time of the attacks so you can cancel resets if a more recent attack has happened (than the one that invoked the reset)”.

Yes, I’m aware of what you were asking. It’s not “prone” to anything, it is what you make it. There’s nothing wrong with delays, you just didn’c combine them with time checks.

While I am sleep deprived, and haven’t tested the code, I’m almost completely sure it should be good as is. Look at the relevant parts again, keep track of the variables in your mind.

1 Like