ClientCast - A Client-based, Idiosyncratic Hitbox System!

game.Players.PlayerAdded:Connect(function(plr)
   plr.CharacterAdded:Connect(function(char)
      while true do
         local lastHRP = char.HumanoidRootPart.Position
         task.wait(2)
         if (char.HumanoidRootPart.Position - lastHRP).Magnitude > 20 then
            char:PivotTo(CFrame.new(lastHRP))
         end
      end
   end)
end)


anyway, I wanted smooth weapons and this is what i will use in the future, thanks

2 Likes

Update 1.0.8.6

  • If a connected client were to have several connected casters at once, a raycast event from the client would fire all active casters on the server which are owned by that client. Oopsā€¦

Is this good for spamming projectiles that are fast aswell?

I wouldnā€™t recommend ClientCast for this, as itā€™s mainly meant for melee hitboxes.

How would i activate raycasts for separate parts? For example, have the left fist activate itā€™s raycasts only, then on the second time the player clicks the right fist activates.

You can create separate Caster objects for each fist, and put damage attachments in each fist respectively.

1 Like

Hey there!

So Iā€™ve been using your module to make a sword system. Works pretty nice, however I noticed one thing.

Whenever I try to set the owner of the sword and do Caster:GetOwner(), it prints out nil? And using Caster:GetPing() prints out 0.

Did I mess something up?

image


Screenshots:

image

image

image


Client:

local RS = game:GetService("ReplicatedStorage")
local PS = game:GetService("Players")

local player = PS.LocalPlayer
local char = player.Character or player.CharacterAdded:Wait()

player.CharacterAppearanceLoaded:Wait()

local hum = char:FindFirstChildWhichIsA("Humanoid")
local animator = hum:FindFirstChildWhichIsA("Animator")
local tool = script.Parent
local blade = tool:WaitForChild("Blade")
local equip = blade:WaitForChild("Equip")
local request = RS.Sword.Events.Request
local validate = RS.Sword.Events.Validate
local idleAnim = nil
local animation = {
	attack = {
		9503532906,
		9503922539,
		9504030319
	},
	idle = 9504150874,
}

local function LoadAnimation(id, priority)
	local anim = Instance.new("Animation") do
		anim.AnimationId = "rbxassetid://" .. id
	end
	
	local load = animator:LoadAnimation(anim) do
		load.Priority = priority
		load:Play()
	end
	
	return load
end

tool.Equipped:Connect(function()
	equip:Play()
	idleAnim = LoadAnimation(animation.idle, Enum.AnimationPriority.Idle)
end)

tool.Unequipped:Connect(function()
	if idleAnim then
		idleAnim:Stop()
		idleAnim = nil
	end
end)

tool.Activated:Connect(function()
	request:FireServer(tool)
end)

request.OnClientEvent:Connect(function()
	LoadAnimation(animation.attack[math.random(1, #animation.attack)], Enum.AnimationPriority.Action)
end)

Server:

local RS = game:GetService("ReplicatedStorage")

local caster = require(RS.Sword.ClientCast)
local request = RS.Sword.Events.Request
local validate = RS.Sword.Events.Validate
local stat = {
	Sword1 = {Damage = 10}
}
local debounce = {}

local function IsHitboxValid(hitPos: Vector3, swordPos: Vector3): boolean
	local mag = (hitPos - swordPos).Magnitude

	if mag > 8 then
		return false
	else
		return true
	end
end

local function CalculateHitbox(player, blade)
	if not player or not blade then
		return
	end

	local sword = caster.new(blade, RaycastParams.new())
	local char = player.Character or player.CharacterAdded:Wait()
	local debounce = {}

	sword.HumanoidCollided:Connect(function(ray, hum)
		local instance = ray.Instance
		
		if instance:IsDescendantOf(char) or instance:IsDescendantOf(blade.Parent) or IsHitboxValid(ray.Position, sword.Object.Position) == false then
			return
		end 
		
		local info = stat[blade.Parent.Name]
		local hit = blade:FindFirstChild("Hit")
		
		if debounce[hum] then
			return
		end

		debounce[hum] = true

		hit:Play()
		hum:TakeDamage(info.Damage)
	end)

	task.spawn(function()
		warn("old: ", sword:GetOwner())
		sword:SetOwner(player)
		warn("new: ", sword:GetOwner())
		warn("ping: ", sword:GetPing())
		sword:Start()
		task.wait(1)
		sword:Destroy()
	end)
end

request.OnServerEvent:Connect(function(player: Player, tool: Tool)
	if debounce[player] or not tool:FindFirstChild("Blade") then
		return
	end

	local blade = tool:FindFirstChild("Blade")

	debounce[player] = true
	
	request:FireClient(player)
	CalculateHitbox(player, blade)

	task.wait(2)
	debounce[player] = false
end)

Iā€™ve been trying to use this for a few hours, and got a bug.

Whenever I first join the game, hitboxes are fine, but when I respawn and try to hit someone, the first hit registers, but then it just breaks and there are no errors, or it just registers a few hits.

Any ways to fix it?

Hey, sorry for the late response. Whatā€™s happening is that to make sure the :SetOwner command is replicated to the client, :SetOwner is actually wrapped in task.spawn, which then yields for a maximum of 0.1 seconds. This is to make sure that by the time the SetOwner command is sent to the client, the client already has a Caster object created.
If you want the casterā€™s object to be set to player the moment itā€™s created, use the third argument of ClientCast.new directly:

Sorry for the confusion!

EDIT: Due to how I structured the code, it should actually be fine to remove the yields in theory. I will update the module to remove the yields, which should fix your issue without having to do anything besides updating the module.

1 Like

Please verify that this is indeed an issue with the ClientCast module, and not your code. ClientCast never indexes the Character property in any of itā€™s code, meaning itā€™s extremely unlikely for the bug to be tied to ClientCast itself.

I was using Caster:SetOwner(Player), but once that I removed the set owner line of code, everything started working fine again.

But now the hitbox is server sided and not client sided as Iā€™d like to.

Iā€™m not sure what the issue might be, but I recommend creating a separate studio place, and try reproducing the bug with as few lines of code as possible. Itā€™ll become much easier to identify the problem from there.

figured a lot out but now im running into a separate issue.

Image from Gyazo

I realised that if my character stays in the same position after being hit, none of the other hits register. I troubleshooted a lot and it turns out that this is a problem with client cast itself.

ClientCast only raycasts, it has no debounces, cooldowns, and the like. I really donā€™t think ClientCast is the issue here, but if youā€™re sure that it is, please provide a file which reproduces your issue in as few lines as possible.

Small script that uses a tool to use clientcast. It works fine at first, but whenever I respawn, it stops working at all. This is a simple script and it should still be working even after respawning if im not wrong, I donā€™t think I used something the wrong way

local clientCast = require(game.ReplicatedStorage.ClientCast)

local hitbox = clientCast.new(script.Parent.Handle, RaycastParams.new(), script.Parent.Parent.Parent)

local alreadyHit = false

hitbox.Collided:Connect(function(params)
	if alreadyHit then return end
	if params.Instance.Parent:FindFirstChild("Humanoid") then
		alreadyHit = true
		params.Instance.Parent:FindFirstChild("Humanoid"):TakeDamage(10)
	end	
		
end)

local inCD = false

function attack()
	if inCD then return end
	
	inCD = true
	task.spawn(function()
		wait(1)
		alreadyHit = false
		inCD = false
	end)
	
	hitbox:Start()
	spawn(function()
		wait(.5)
		hitbox:Stop()
	end)
end

script.Parent.Activated:Connect(function()
	attack()
end)

Could you make this a .rbxl file with the tool and code in it? Since I donā€™t know what the tool is.

Itā€™s a simple brick with a dmgPoint inside it
clientCastTest.rbxl (43.9 KB)

1 Like

Interesting catch! This seems to be a mix of buggy behavior between both ClientCast and Roblox itself - since the server canā€™t access Player.PlayerScripts, it parents the ClientHandler LocalScript into PlayerGui to run the code, and then ClientHandler reparents itself into Player.PlayerScripts. The deprecated StarterGui.ResetPlayerGuiOnSpawn conflicts with this though, and purges any children that were parented into PlayerGui at any point in time, even if they were later reparented.
Iā€™ll try finding a better workaround for this, but for now, you can fix this by running game:GetService("StarterGui").ResetPlayerGuiOnSpawn = false in the command bar. Sorry for the inconvenience!

All good, Iā€™m not sure how to fix it so I will wait for you to push a fix. I msut use resetplayerguionspawn because I have important guis that need to be resetted when respawned. Nice one on noticing the bug so fast haha

1 Like

Hey! The issue should now be fixed on GitHub, though Iā€™m reworking ClientCast a bit to support Rojo, along with adding a brand new documentation API so I wonā€™t update the devforum thread for a bit.
You can get the newest update here for now:
https://github.com/PysephWasntAvailable/ClientCast
image

(cc. @Downrest, your issue should also be fixed now)

1 Like