ClientCast - A Client-based, Idiosyncratic Hitbox System!

Oh cool alr, thx for letting me know. Il try that and if ut doest work maybe ill tey to pm the OP

yeah okay, i tried all types of CollisionFidelity, but none of them worked. Thx for trying to help though.

nvm i think i realized the reason why itā€™s not properly working for me. The problem is that my enemy is too tall. Roblox added a feature long time ago where the humanoid root partā€™s bottom aligns with the hip of a rig. For that reason, when the character swings the sword sometimes it doesnā€™t hit the HumanoidRootPart of the enemy (only their legs) and doesnā€™t trigger HumanoidCollided.

This is weird. HumanoidCollided should (to my limited knowledge of ClientCast internal workings) fire when any of the characterā€™s parts were collided, not only the root part.

You should definitely PM the creator a repro file to find out if it is either a bug in ClientCast or a setup error.

1 Like

I figured out my problem, the meshparts had CanQuery off. It was not showing me the option because CanCollide was on.

2 Likes

So Iā€™m having an issue where if I set the owner of a cast to a player then if they have a frame drop then it is a lot less accurate and having itā€™s owner to the client in general seems to make it a lot less accurate.
The way I solved this was by setting the owner of the cast to the server and the cast is way more accurate and consistent now though it created another problem. Now if you start running or moving quickly then use your weapon the cast is coming from where the server sees your character rather than where you see your character.
Is there a way to make it run on the client with the accuracy of the server?
Not even sure if what Iā€™m asking is even possible or makes much sense.

Thatā€™s not possible, no. The reason the client isnā€™t as accurate is because the client isnā€™t running at 60 FPS like the server - my best advice would be to try checking if your game has any performance issues on the client, and fix them.

1 Like

Hello, Iā€™m having a fairly big issue with the module. The caster works perfectly fine when a player first joins the game and spawns, however when the player resets or dies any future use of a new Caster will cause an error in the ClientCast module:


It seems like an issue with the reference of the old caster not being cleared, which is weird because I use the :Destroy() method on the ClientCaster object, which should completely remove the object and all its connections according to the API on the ClientCast wiki. It also doesnā€™t happen every single time the player dies, it only happens half the time, which just confuses me even more.

Hereā€™s the rest of the code (for reference, this is a Module Script):

----- Services -----

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local Debris = game:GetService("Debris")

----- Modules -----

local ClientCast = require(ServerScriptService.ClientCast)

----- Variables -----

local PlayerList = {} -- PlayerName = {Caster, NextSwing, LastAttack, LastParry, LastFeint, FeintPossible, Feinting, Debounce, HitPlayers}

----- Functions -----

local function ClaymoreUnequip(Player)
	if PlayerList[Player] ~= nil then
		PlayerList[Player].Caster:Destroy() -- Destroys Caster to prevent memory leak
		task.wait()
		PlayerList[Player] = nil
	end
end

local function ClaymoreEquip(Player, Tool)
	if PlayerList[Player] == nil then

		local Character = Player.Character
		if Character == nil then return end
		local Humanoid = Character:FindFirstChild("Humanoid")
		if Humanoid == nil or Humanoid.Health == 0 then return end

		local RayParams = RaycastParams.new()
		RayParams.FilterDescendantsInstances = Character:GetDescendants()
		RayParams.FilterType = Enum.RaycastFilterType.Blacklist
		local ClientCaster = ClientCast.new(Tool:FindFirstChild("Blade"), RayParams)
		ClientCaster:SetOwner(Player)
		
		ClientCaster.HumanoidCollided:Connect(function(RaycastResult, HitHumanoid)
			if PlayerList[Player].HitPlayers[HitHumanoid] then return end

			local EnemyRootPart = HitHumanoid.Parent.PrimaryPart
			local PlayerRootPart = Character.PrimaryPart
			if (EnemyRootPart.CFrame.Position - PlayerRootPart.CFrame.Position).Magnitude > 11 then return end

			PlayerList[Player].HitPlayers[HitHumanoid] = true
			HitHumanoid:TakeDamage(22)
			EnemyRootPart:ApplyImpulse((EnemyRootPart.CFrame.Position - Character.PrimaryPart.CFrame.Position).Unit * 2000)
			
			task.wait(0.3)

			PlayerList[Player].HitPlayers[HitHumanoid] = false
		end)
		
		Humanoid.Died:Connect(function() -- Removes player from table when they die to free memory
			ClaymoreUnequip(Player)
		end)
		
		PlayerList[Player] = {Caster = ClientCaster, NextSwing = 1, LastAttack = 0, LastParry = 0, LastFeint = 0, FeintPossible = false, Feinting = false, Debounce = false, HitPlayers = {}}
		
	end
end

local function ClaymoreAttack(Player, Tool)
	if PlayerList[Player].Debounce == true then return end
	PlayerList[Player].Debounce = true

	if os.clock() - PlayerList[Player].LastAttack > 0.5 then
		if os.clock() - PlayerList[Player].LastAttack < 1.5 or os.clock() - PlayerList[Player].LastFeint < 0.5 then
			PlayerList[Player].Debounce = false
			return 
		end
		PlayerList[Player].NextSwing = 1
	end	

	if PlayerList[Player].NextSwing == 1 then

		PlayerList[Player].NextSwing = 2
		task.wait(.25)
		PlayerList[Player].Caster:Start()
		task.wait(.25)

	elseif PlayerList[Player].NextSwing == 2 then

		PlayerList[Player].NextSwing = 3
		task.wait(.16)
		PlayerList[Player].Caster:Start()
		task.wait(.25)

	elseif PlayerList[Player].NextSwing == 3 then

		PlayerList[Player].NextSwing = 2
		task.wait(.25)
		PlayerList[Player].Caster:Start()
		task.wait(.25)
		
	end
	
	PlayerList[Player].Caster:Stop()
	PlayerList[Player].Debounce = false
end

----- Connections -----

Players.PlayerRemoving:Connect(function(Player)
	if PlayerList[Player] ~= nil then
		PlayerList[Player].Caster:Destroy()
		PlayerList[Player] = nil
	end
end)

ClaymoreModule = {}

function ClaymoreModule:StartAction(Player, ActionType)
	local Character = Player.Character
	if Character then

		if ActionType == "Unequipping" then

			ClaymoreUnequip(Player)

		else

			local Claymore = Character:WaitForChild("Claymore", 1)
			if Claymore then

				if ActionType == "Equipping" then
					ClaymoreEquip(Player, Claymore)
				elseif ActionType == "Attacking" then
					ClaymoreAttack(Player, Claymore)
				elseif ActionType == "Parrying" then
					-- ClaymoreParry(Player, Claymore)
				elseif ActionType == "Feinting" then
					-- ClaymoreFeint(Player, Claymore)
				end
				
			end
		end
	end
end

return ClaymoreModule

Any help would be appreciated and bear with me please, as Iā€™m not an extremely experienced coder.

PS: I also noticed something peculiar when playing around with printing:

		print("Destroying players caster!")
		PlayerList[Player].Caster:Destroy()
		print("Players caster destroyed!")
		task.wait()
		print(PlayerList[Player].Caster)
		PlayerList[Player] = nil

The output:


Maybe Iā€™m not well-versed enough to understand why this is happening?

1 Like

The provided code is a bit large - I recommend making a more minimal reproduction script, such as creating a new baseplate and adding the minimal amount of code needed to reproduce that bug.

local Players = game:GetService("Players")
local ClientCast = require(game:GetService("ServerScriptService").ClientCast)
local RemoteEvent = game:GetService("ReplicatedStorage").RemoteEvent

local Table = {} -- Player = Caster

local function StartCasting(Player)
	Table[Player].Caster:Start()
	task.wait(10)
	Table[Player].Caster:Stop()
end

Players.PlayerAdded:Connect(function(Player)
	Player.CharacterAdded:Connect(function(Character)
		local Humanoid = Character:WaitForChild("Humanoid", 1)

		local ClientCaster = ClientCast.new(workspace.ExamplePart1, RaycastParams.new())
		ClientCaster:SetOwner(Player)
		ClientCaster:StartDebug()

		ClientCaster.HumanoidCollided:Connect(function(RaycastResult, HitHumanoid)
			print("Hit model:" .. HitHumanoid.Parent.Name)
		end)

		Table[Player] = {Caster = ClientCaster}

		Humanoid.Died:Connect(function()
			Table[Player].Caster:Destroy()
			task.wait()
			Table[Player] = nil
		end)
	end)
end)

RemoteEvent.OnServerEvent:Connect(StartCasting)

This is the smallest I could get while keeping the structure relatively the same (hopefully thatā€™s enough). This still causes the same error I posted above whenever the player dies and gets a new table entry and caster. This code is also no longer in a Module Script, so itā€™s not caused by that either. I also noticed now that the error only appears when the HumanoidCollided event is fired, not when casting starts or stops.

At what intervals is the remote fired? Could you replace it with a manual loop?

The remote was fired manually by me via UserInputService on a local script, but hereā€™s the same script with a loop instead:

local Players = game:GetService("Players")
local ClientCast = require(game:GetService("ServerScriptService").ClientCast)
local RemoteEvent = game:GetService("ReplicatedStorage").RemoteEvent

local Table = {} -- Player = Caster

local function StartCasting(Player)
	while Table[Player] ~= nil do
		print("Started casting")
		Table[Player].Caster:Start()
		task.wait(5)
		if Table[Player] ~= nil then
			print("Stopped casting")
			Table[Player].Caster:Stop()
			task.wait(5)
		end
	end
end

Players.PlayerAdded:Connect(function(Player)
	Player.CharacterAdded:Connect(function(Character)
		local Humanoid = Character:WaitForChild("Humanoid", 1)

		local ClientCaster = ClientCast.new(workspace.ExamplePart1, RaycastParams.new())
		ClientCaster:SetOwner(Player)
		ClientCaster:StartDebug()

		ClientCaster.HumanoidCollided:Connect(function(RaycastResult, HitHumanoid)
			print("Hit model:" .. HitHumanoid.Parent.Name)
		end)

		Table[Player] = {Caster = ClientCaster}
		task.spawn(function()
			StartCasting(Player)
		end)

		Humanoid.Died:Connect(function()
			Table[Player].Caster:Destroy()
			task.wait()
			Table[Player] = nil
		end)
	end)
end)

For some reason this doesnā€™t cause the same error. Unfortunately thatā€™s not exactly helpful to me, as I canā€™t rewrite the script without knowing what even causes the error in the first placeā€¦

1 Like

v1.13.0

  • Remove ClientCaster:GetPing
  • Remove ClientCaster:GetMaxPingExhaustion
  • Remove ClientCaster:SetMaxPingExhaustion
    • Sidenote: You should use Player:GetNetworkPing() now instead.
  • Update raycast data serialization to take up less space
2 Likes

Could you add more examples?

(Sw0kw)

I read all the introductions and comments, but found that only attachment can be used instead of bone. Itā€™s a pity. I look forward to adding bones

Hey! Adding support for bones is definitely possible; Iā€™ll look into it in the nearby future.

v1.14.0

  • Added support for Bone instances being used as DmgPoint attachments
    cc. @dahuilang_1

God, Iā€™m so glad that you added this so soon. I tried to use RaycastHitboxV4 before. As you said, it also has server latency problems. I will use the new version immediately and give you feedback. Thank you for updating so quickly

Hi, my friend, did you submit the wrong version? I donā€™t think the code supports bone. In the OnDamagePointAdded function, there is only Attachment

hi,friendļ¼ŒI changed your module to support bones, and then tested it in the game. I think ClientCast is better than the RaycastHitboxV4 (because the Animation Backtracks of your module will not have much server delay, and the performance is very good,This should be the best melee attack system Iā€™ve ever used). I hope you can continue to optimize