Combat System Using Raycasts Not Efficient

Hey devs,

I made a sword fighting system using hitboxes and raycasts. A huge issue right now is that during combat the detection system barely picks up any hits (even though you can clearly see the player being slashed with the sword). When I hit a dummy it is fine (because it’s immobile), but when you try and hit a moving player, it barely detects.

Please take a look at the code and tell me if there are any improvements I can make.

ServerScript:

local SS = game:GetService("SoundService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

local CombatEvent = ReplicatedStorage.Remotes.CombatEvent

Players.PlayerAdded:Connect(function(player)
	player.CharacterAdded:Connect(function(character)
		local CombatStatus = Instance.new("StringValue", character)
		CombatStatus.Name = "SwordStatus"
		CombatStatus.Value = ""

		local Hitbox = Instance.new("Part")
		Hitbox.Name = "Hitbox"
		Hitbox.Anchored = false
		Hitbox.Massless = true
		Hitbox.CanCollide = false
		Hitbox.CanTouch = false
		Hitbox.Transparency = 1
		Hitbox.Size = Vector3.new(7, 6, 7)
		Hitbox.Color = Color3.new(1, 0, 0)
		Hitbox.CFrame = character.HumanoidRootPart.CFrame
		Hitbox.Parent = character.HumanoidRootPart

		local WeldConstraint = Instance.new("WeldConstraint")
		WeldConstraint.Part0 = Hitbox
		WeldConstraint.Part1 = character.HumanoidRootPart
		WeldConstraint.Parent = Hitbox

		Hitbox.CFrame = character.HumanoidRootPart.CFrame * CFrame.new(0, 0, 0)
	end)
end)

CombatEvent.OnServerEvent:Connect(function(plr, eventType, Arg1)
	local Character = plr.Character
	local Humanoid = Character:FindFirstChild("Humanoid")
	local CombatStatus = Character:FindFirstChild("SwordStatus")

	if eventType == "SM1" and CombatStatus.Value == "" then
		CombatStatus.Value = "Attacking"

		local AT = Humanoid.Animator:LoadAnimation(script[Arg1])
		AT:Play()

		local Hitbox = Character.HumanoidRootPart:FindFirstChild("Hitbox")
		local directions = {
			Character.HumanoidRootPart.CFrame.LookVector,
			(Character.HumanoidRootPart.CFrame * CFrame.Angles(0, math.rad(10), 0)).LookVector,
			(Character.HumanoidRootPart.CFrame * CFrame.Angles(0, math.rad(-10), 0)).LookVector
		}

		local hitDetected = false

		for _, direction in ipairs(directions) do
			local ray = workspace:Raycast(Hitbox.Position, direction * 10, RaycastParams.new{
				FilterType = Enum.RaycastFilterType.Exclude,
				FilterDescendantsInstances = { Character }
			})

			if ray and ray.Instance and ray.Instance.Parent:FindFirstChild("Humanoid") then
				local enemyCharacter = ray.Instance.Parent

				if enemyCharacter == Character then
					continue
				end

				hitDetected = true

				if enemyCharacter.SwordStatus.Value == "Blocking" then
					enemyCharacter.SwordStatus.Value = ""
					SS.SwordParry:Play()
					AT:Stop()
					Character.SwordStatus.Value = "Parried"

					task.delay(0.75, function()
						Character.SwordStatus.Value = ""
					end)
				else
					local enemyHumanoidRootPart = enemyCharacter:FindFirstChild("HumanoidRootPart")
					SS.SwordHit:Play()

					if Arg1 == "Stab4" then
						enemyCharacter.Humanoid.Health -= 30
					else
						enemyCharacter.Humanoid.Health -= 10
					end
					print("dealing damage")

					if enemyHumanoidRootPart then
						local knockbackForce = Instance.new("BodyVelocity")
						knockbackForce.MaxForce = Vector3.new(100000, 0, 100000)
						knockbackForce.P = 1000

						if Arg1 == "Stab4" then
							knockbackForce.Velocity = direction * 50
						else
							knockbackForce.Velocity = direction * 30
						end

						knockbackForce.Parent = enemyHumanoidRootPart
						game:GetService("Debris"):AddItem(knockbackForce, 0.2)
					end
				end
				break
			end
		end

		if not hitDetected then
			SS.SwordSlash:Play()
		end

		task.delay(0.75, function()
			if CombatStatus.Value == "Attacking" then
				CombatStatus.Value = ""
				print("forced stop")
			end
		end)
	elseif eventType == "Block" and (Character.SwordStatus.Value == "" or Character.SwordStatus.Value == "Blocking") then
		if Character.SwordStatus.Value == "" then
			Character.SwordStatus.Value = "Blocking"

			local BlockAnimation = Humanoid.Animator:LoadAnimation(script.Block)
			BlockAnimation:Play()

			local UpdateFunction
			UpdateFunction = Character.SwordStatus.Changed:Connect(function(newStatus)
				if newStatus ~= "Blocking" then
					BlockAnimation:Stop()
					UpdateFunction:Disconnect()
				end
			end)

			task.delay(1, function()
				if Character.SwordStatus.Value == "Blocking" then
					Character.SwordStatus.Value = ""
					print("Blocking ended automatically")
				end
			end)
		end
	elseif Character.SwordStatus.Value == "Blocking" then
		Character.SwordStatus.Value = ""
	end
end)

ClientScript (Inside every sword):

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

local Tool = script.Parent
local CombatEvent = ReplicatedStorage.Remotes.CombatEvent

local Player = Players.LocalPlayer
local AttackNumber = 1

Tool.Activated:Connect(function()
	if Player.Character.SwordStatus.Value == "" then
		if AttackNumber == 1 then
			AttackNumber = 2
			CombatEvent:FireServer("SM1", "Stab1")
		elseif AttackNumber == 2 then
			AttackNumber = 3
			CombatEvent:FireServer("SM1", "Stab2")
		elseif AttackNumber == 3 then
			AttackNumber = 4
			CombatEvent:FireServer("SM1", "Stab3")
		elseif AttackNumber == 4 then
			AttackNumber = 1
			CombatEvent:FireServer("SM1", "Stab4")
		end
	end
end)

UIS.InputBegan:Connect(function(input, processed)
	if input.KeyCode == Enum.KeyCode.E and not processed and Player.Character:FindFirstChild(Tool.Name) then
		CombatEvent:FireServer("Block")
	end
end)

Thank you so much.

-DarkPurpleElixr

4 Likes

Instead of using raycasting, use blockcasting. It works similar to raycasting, but essentially, it’s casting a hitbox. It would allow you to only cast it once rather than casting multiple raycasts in different directions, like in your script, which would boost your game’s performance.

2 Likes

Alright! I will give it a try and get back to you.

2 Likes

I tried this code out but it keeps returning nothing was hit, am I doing something wrong?

local SS = game:GetService("SoundService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

local CombatEvent = ReplicatedStorage.Remotes.CombatEvent

Players.PlayerAdded:Connect(function(player)
	player.CharacterAdded:Connect(function(character)
		local CombatStatus = Instance.new("StringValue", character)
		CombatStatus.Name = "SwordStatus"
		CombatStatus.Value = ""

		local Hitbox = Instance.new("Part")
		Hitbox.Name = "Hitbox"
		Hitbox.Anchored = false
		Hitbox.Massless = true
		Hitbox.CanCollide = false
		Hitbox.CanTouch = false
		Hitbox.Transparency = 1
		Hitbox.Size = Vector3.new(7, 6, 7)
		Hitbox.Color = Color3.new(1, 0, 0)
		Hitbox.CFrame = character.HumanoidRootPart.CFrame
		Hitbox.Parent = character.HumanoidRootPart

		local WeldConstraint = Instance.new("WeldConstraint")
		WeldConstraint.Part0 = Hitbox
		WeldConstraint.Part1 = character.HumanoidRootPart
		WeldConstraint.Parent = Hitbox

		Hitbox.CFrame = character.HumanoidRootPart.CFrame * CFrame.new(0, 0, 0)
	end)
end)

CombatEvent.OnServerEvent:Connect(function(plr, eventType, Arg1)
	local Character = plr.Character
	local Humanoid = Character:FindFirstChild("Humanoid")
	local CombatStatus = Character:FindFirstChild("SwordStatus")

	if eventType == "SM1" and CombatStatus.Value == "" then
		CombatStatus.Value = "Attacking"

		local AT = Humanoid.Animator:LoadAnimation(script[Arg1])
		AT:Play()

		local Hitbox = Character.HumanoidRootPart:FindFirstChild("Hitbox")
		local blockSize = Vector3.new(7, 6, 7)
		local blockDirection = Character.HumanoidRootPart.CFrame.LookVector * 10

		local raycastParams = RaycastParams.new()
		raycastParams.FilterType = Enum.RaycastFilterType.Exclude
		raycastParams.FilterDescendantsInstances = { Character }

		local blockcastResult = workspace:Blockcast(Hitbox.CFrame, blockSize, blockDirection, raycastParams)

		if blockcastResult and blockcastResult.Instance then
			local hitPart = blockcastResult.Instance
			local enemyCharacter = hitPart.Parent

			print("Blockcast hit: ", hitPart:GetFullName())

			if enemyCharacter and enemyCharacter:FindFirstChild("Humanoid") then
				print("Enemy detected:", enemyCharacter.Name)

				if enemyCharacter.SwordStatus.Value == "Blocking" then
					enemyCharacter.SwordStatus.Value = ""
					SS.SwordParry:Play()
					AT:Stop()
					Character.SwordStatus.Value = "Parried"

					task.delay(0.75, function()
						Character.SwordStatus.Value = ""
					end)
				else
					local enemyHumanoidRootPart = enemyCharacter:FindFirstChild("HumanoidRootPart")
					SS.SwordHit:Play()

					if Arg1 == "Stab4" then
						enemyCharacter.Humanoid.Health -= 30
					else
						enemyCharacter.Humanoid.Health -= 10
					end
					print("Dealing damage to:", enemyCharacter.Name)

					if enemyHumanoidRootPart then
						local knockbackForce = Instance.new("BodyVelocity")
						knockbackForce.MaxForce = Vector3.new(100000, 0, 100000)
						knockbackForce.P = 1000

						if Arg1 == "Stab4" then
							knockbackForce.Velocity = blockDirection * 50
						else
							knockbackForce.Velocity = blockDirection * 30
						end

						knockbackForce.Parent = enemyHumanoidRootPart
						game:GetService("Debris"):AddItem(knockbackForce, 0.2)
					end
				end
			else
				print("No enemy detected or SwordStatus is not found.")
			end
		else
			print("Nothing was hit")
			SS.SwordSlash:Play()
		end

		task.delay(0.75, function()
			if CombatStatus.Value == "Attacking" then
				CombatStatus.Value = ""
				print("Forced stop")
			end
		end)
	elseif eventType == "Block" and (Character.SwordStatus.Value == "" or Character.SwordStatus.Value == "Blocking") then
		if Character.SwordStatus.Value == "" then
			Character.SwordStatus.Value = "Blocking"

			local BlockAnimation = Humanoid.Animator:LoadAnimation(script.Block)
			BlockAnimation:Play()

			local UpdateFunction
			UpdateFunction = Character.SwordStatus.Changed:Connect(function(newStatus)
				if newStatus ~= "Blocking" then
					BlockAnimation:Stop()
					UpdateFunction:Disconnect()
				end
			end)

			task.delay(1, function()
				if Character.SwordStatus.Value == "Blocking" then
					Character.SwordStatus.Value = ""
					print("Blocking ended automatically")
				end
			end)
		end
	elseif Character.SwordStatus.Value == "Blocking" then
		Character.SwordStatus.Value = ""
	end
end)

Where is the hitbox relative to the HumanoidRootPart?

It is centered to the HumanoidRootPart. Here is an image (you can see the output too):

Ok I was just swinging around and I did get a hit, but the detection is not good:
12:45:41.029 Blockcast hit: Workspace.Rig.LeftUpperArm - Server - CombatScript:203

Try changing the direction magnitude to about 2 instead of 10, and let me know what happens.

The detection is much better. The issue is that if the dummy is inside the hitbox, it cant be hit. I can only hit the dummy at a certain position.

Another growing issue is this:

I am trying to hit the dummy, but it keeps getting the other parts. Giving some code to fix this would be nice

Thanks.

Actually don’t worry about the second issue I mentioned above. I fixed that.

1 Like

In that case, just remove the multiplier part and just have:
local blockDirection = Character.HumanoidRootPart.CFrame.LookVector

Why not use workspace:GetPartsInParts()?

Yep better.

But still if the player were any closer than this:

it still doesn’t detect. Only if the player is at that exact distance it works. If you go closer it doesn’t detect for some reason.

I will try that if this system doesn’t work.

Just saying Blockcast don’t detect parts that are already in the box when it was created Direct quote from Blockcast Documentation: “Unlike WorldRoot:GetPartsInPart(), this method does not detect BaseParts that initially intersect the shape.” Unless I’m reading that wrong

1 Like

Huh. Looks like you are right. I will try something out and see if it works.

Oh yeah, I forgot about that. So then, :GetPartsInPart() would be a better option.

Try to turn off respect to can collide in the raycast perams.

Raycasts with CanCollide checking - Resources / Community Resources - Developer Forum | Roblox

Thanks! Worked like a charm.

Char limit

Sorry for pushing back up; there is a new bug.

For some reason, the hit is only happening every other time. Does anyone know a fix for this?

Server Code:

local Debris = game:GetService("Debris")

local SS = game:GetService("SoundService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local Workspace = game:GetService("Workspace")

local Tool = script.Parent

local animationIds = {
	["Stab1"] = "rbxassetid://18894699991",
	["Stab2"] = "rbxassetid://18894701886",
	["Stab3"] = "rbxassetid://18894703743",
	["Stab4"] = "rbxassetid://18894705958",
}

local currentAnimation = "Stab1"

local function GetNextAnimation(current)
	local animations = {"Stab1", "Stab2", "Stab3", "Stab4"}
	local nextIndex = 1

	for i, anim in ipairs(animations) do
		if anim == current then
			nextIndex = i + 1
			break
		end
	end

	if nextIndex > #animations then
		nextIndex = 1
	end

	return animations[nextIndex]
end

function TagHumanoid(humanoid, player)
	local Creator_Tag = Instance.new("ObjectValue")
	Creator_Tag.Name = "creator"
	Creator_Tag.Value = player
	Debris:AddItem(Creator_Tag, 2)
	Creator_Tag.Parent = humanoid
end

function UntagHumanoid(humanoid)
	for i, v in pairs(humanoid:GetChildren()) do
		if v:IsA("ObjectValue") and v.Name == "creator" then
			v:Destroy()
		end
	end
end

local function OnToolActivated(tool)
	local swordStatus = Character:FindFirstChild("SwordStatus")

	if not Humanoid or not swordStatus then
		return
	end

	if swordStatus.Value == "" then
		swordStatus.Value = "Attacking"

		local animation = Instance.new("Animation")
		animation.AnimationId = animationIds[currentAnimation]
		local animationTrack = Humanoid.Animator:LoadAnimation(animation)
		animationTrack:Play()

		currentAnimation = GetNextAnimation(currentAnimation)

		animationTrack.Stopped:Connect(function()
			if swordStatus.Value == "Attacking" then
				swordStatus.Value = ""
				print("Animation ended, forced stop")
			end
		end)

		local hitbox = Character:FindFirstChild("HumanoidRootPart"):FindFirstChild("Hitbox")

		local overlapParams = OverlapParams.new()
		overlapParams.FilterType = Enum.RaycastFilterType.Exclude
		overlapParams.FilterDescendantsInstances = { Character }
		overlapParams.MaxParts = 10

		local partsInHitbox = Workspace:GetPartsInPart(hitbox, overlapParams)

		local hitDetected = false
		for _, part in ipairs(partsInHitbox) do
			if part:IsA("BasePart") and part.Parent ~= Character then
				local enemyCharacter = part.Parent

				print("Hitbox detected: ", part:GetFullName())

				if enemyCharacter and enemyCharacter:FindFirstChild("Humanoid") then
					print("Enemy detected:", enemyCharacter.Name)

					if enemyCharacter:FindFirstChildOfClass("ForceField") then
						print("Enemy has a ForceField, skipping damage and knockback")
						continue
					end

					local enemyHumanoidRootPart = enemyCharacter:FindFirstChild("HumanoidRootPart")
					SS.SwordHit:Play()

					local damage = (currentAnimation == "Stab4") and 30 or 10
					enemyCharacter.Humanoid:TakeDamage(damage)
					print("Dealing damage to:", enemyCharacter.Name)

					UntagHumanoid(enemyCharacter.Humanoid)
					TagHumanoid(enemyCharacter.Humanoid, Player)

					if enemyHumanoidRootPart then
						local knockbackForce = Instance.new("BodyVelocity")
						knockbackForce.MaxForce = Vector3.new(100000, 0, 100000)
						knockbackForce.P = 1000
						knockbackForce.Velocity = hitbox.CFrame.LookVector * ((currentAnimation == "Stab4") and 50 or 30)

						knockbackForce.Parent = enemyHumanoidRootPart
						game:GetService("Debris"):AddItem(knockbackForce, 0.2)
					end

					hitDetected = true
					break
				end
			end
		end

		if not hitDetected then
			print("Nothing was hit")
			SS.SwordSlash:Play()
		end
	end
end

local function onBlockEvent(player)
	local character = player.Character
	if not character then return end

	local humanoid = character:FindFirstChildOfClass("Humanoid")
	local combatStatus = character:FindFirstChild("SwordStatus")

	if humanoid and combatStatus then
		if combatStatus.Value == "" or combatStatus.Value == "Blocking" then
			if combatStatus.Value == "" then
				combatStatus.Value = "Blocking"
				local blockAnimation = humanoid.Animator:LoadAnimation(script.Block)
				blockAnimation:Play()

				wait(0.5)
				if combatStatus.Value == "Blocking" then
					combatStatus.Value = ""
					blockAnimation:Stop()
					print("Blocking ended automatically after 0.5 seconds")
				end

				local updateConnection
				updateConnection = combatStatus.Changed:Connect(function(newStatus)
					if newStatus ~= "Blocking" then
						blockAnimation:Stop()
						updateConnection:Disconnect()
					end
				end)

				blockAnimation.Stopped:Connect(function()
					if combatStatus.Value == "Blocking" then
						combatStatus.Value = ""
						print("Blocking ended automatically")
					end
				end)
			end
		else
			combatStatus.Value = ""
		end
	end
end


script.Parent.BlockEvent.OnServerEvent:Connect(onBlockEvent)

local function OnToolEquipped()
	Character = Tool.Parent
	Player = Players:GetPlayerFromCharacter(Character)
	Humanoid = Character:FindFirstChildOfClass("Humanoid")
end

Tool.Activated:Connect(OnToolActivated)
Tool.Equipped:Connect(OnToolEquipped)

Players.PlayerAdded:Connect(function(player)
	player.CharacterAdded:Connect(function(character)
		
	end)
end)

Thanks for helping!