How do I make my knife throwing system less jittery and buggy?

External Media

As you can see, when I throw the knife it freezes mid air and it bounces off the floor even after while I turned its CanCollide = false

GunModulescript:

local GunModule = {}

GunModule.Config = {
	Damage = 10000,
	FireCooldown = 3.065,
	MaxRange = 500,
	RateLimit = 20,
	RateLimitWindow = 5,
	FireCursorIcon = "rbxasset://textures/GunCursor.png",
	ReloadCursorIcon = "rbxasset://textures/GunWaitCursor.png",
}

function GunModule.IsInRange(shooterPos: Vector3, targetPos: Vector3): boolean
	return (shooterPos - targetPos).Magnitude <= GunModule.Config.MaxRange
end

return GunModule

GunService (serverscript)

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

local GunModule = require(ReplicatedStorage.Shared.Modules.GunModule)

local Remotes = ReplicatedStorage.Remotes
local FireGunRemote = Remotes.FireGun
local GunHitRemote = Remotes.GunHit
local PlayerEliminatedRemote = Remotes.PlayerEliminated

local Config = GunModule.Config

local playerFireCooldowns: {[number]: number} = {}
local playerRateLimitData: {[number]: {count: number, windowStart: number}} = {}
local roundActive: boolean = false

local function getRoundActive(): boolean
	return roundActive
end

local function setRoundActive(state: boolean)
	roundActive = state
	if not state then
		table.clear(playerFireCooldowns)
	end
end

local function isOnCooldown(userId: number): boolean
	local lastFire = playerFireCooldowns[userId]
	if not lastFire then return false end
	return (os.clock() - lastFire) < Config.FireCooldown
end

local function setCooldown(userId: number)
	playerFireCooldowns[userId] = os.clock()
end

local function isRateLimited(userId: number): boolean
	local now = os.clock()
	local data = playerRateLimitData[userId]

	if not data then
		playerRateLimitData[userId] = { count = 1, windowStart = now }
		return false
	end

	if (now - data.windowStart) >= Config.RateLimitWindow then
		playerRateLimitData[userId] = { count = 1, windowStart = now }
		return false
	end

	data.count += 1

	if data.count > Config.RateLimit then
		return true
	end

	return false
end

local function validatePlayerHasGun(player: Player): boolean
	local character = player.Character
	if not character then return false end

	local tool = character:FindFirstChildOfClass("Tool")
	if not tool then return false end

	local toolType = tool:FindFirstChild("ToolType")
	if not toolType or toolType.Value ~= "Gun" then return false end

	return true
end

local function killPlayer(killer: Player, victim: Player)
	local victimCharacter = victim.Character
	if not victimCharacter then return end

	local victimHumanoid = victimCharacter:FindFirstChildOfClass("Humanoid")
	if not victimHumanoid then return end
	if victimHumanoid.Health <= 0 then return end

	victimHumanoid:TakeDamage(Config.Damage)

	PlayerEliminatedRemote:FireAllClients(killer, victim)
end

local function processShot(player: Player, targetPosition: Vector3)
	local userId = player.UserId

	-- if not getRoundActive() then return end

	if isRateLimited(userId) then return end
	if isOnCooldown(userId) then return end
	if not validatePlayerHasGun(player) then return end

	local shooterCharacter = player.Character
	if not shooterCharacter then return end

	local shooterHRP = shooterCharacter:FindFirstChild("HumanoidRootPart")
	if not shooterHRP then return end

	local shooterHumanoid = shooterCharacter:FindFirstChildOfClass("Humanoid")
	if not shooterHumanoid or shooterHumanoid.Health <= 0 then return end

	if not GunModule.IsInRange(shooterHRP.Position, targetPosition) then return end

	setCooldown(userId)

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

	local shootOrigin = shooterHRP.Position + Vector3.new(0, 1.5, 0)
	local direction = (targetPosition - shootOrigin).Unit * Config.MaxRange
	local result = workspace:Raycast(shootOrigin, direction, raycastParams)

	if result and result.Instance then
		local hitCharacter = result.Instance:FindFirstAncestorOfClass("Model")

		if hitCharacter then
			local victimPlayer = Players:GetPlayerFromCharacter(hitCharacter)

			if victimPlayer and victimPlayer ~= player then
				local victimHumanoid = hitCharacter:FindFirstChildOfClass("Humanoid")

				if victimHumanoid and victimHumanoid.Health > 0 then
					killPlayer(player, victimPlayer)
					GunHitRemote:FireAllClients(player, result.Position)
				end
			end
		end
	end
end

FireGunRemote.OnServerEvent:Connect(function(player: Player, targetPosition: any)
	if typeof(targetPosition) ~= "Vector3" then return end
	processShot(player, targetPosition)
end)

Players.PlayerRemoving:Connect(function(player: Player)
	local userId = player.UserId
	playerFireCooldowns[userId] = nil
	playerRateLimitData[userId] = nil
end)

local GunService = {}

function GunService.SetRoundActive(state: boolean)
	setRoundActive(state)
end

function GunService.GetRoundActive(): boolean
	return getRoundActive()
end

function GunService.ResetPlayer(player: Player)
	local userId = player.UserId
	playerFireCooldowns[userId] = nil
	playerRateLimitData[userId] = nil
end

return GunService

Gun Client (local script)

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

local GunModule = require(ReplicatedStorage.Shared.Modules.GunModule)

local Remotes = ReplicatedStorage.Remotes
local FireGunRemote = Remotes.FireGun
local GunHitRemote = Remotes.GunHit
local PlayerEliminatedRemote = Remotes.PlayerEliminated

local LocalPlayer = Players.LocalPlayer
local Camera = workspace.CurrentCamera
local Tool = script.Parent

local Config = GunModule.Config

local ShootSound = ReplicatedStorage.Assets.Sounds.Shoot

local FIRE_COOLDOWN = GunModule.Config.FireCooldown

local isEquipped = false
local canFire = true
local mouse = nil

local function updateCursor()
	if not mouse then return end
	if not canFire then
		mouse.Icon = Config.ReloadCursorIcon
	else
		mouse.Icon = Config.FireCursorIcon
	end
end

local function getTargetPosition(): Vector3
	local mousePos = mouse and mouse.Hit and mouse.Hit.Position
	if mousePos then
		return mousePos
	end

	local viewportSize = Camera.ViewportSize
	local ray = Camera:ViewportPointToRay(viewportSize.X / 2, viewportSize.Y / 2)

	local raycastParams = RaycastParams.new()
	local localCharacter = LocalPlayer.Character
	if localCharacter then
		raycastParams.FilterDescendantsInstances = {localCharacter}
		raycastParams.FilterType = Enum.RaycastFilterType.Exclude
	end

	local result = workspace:Raycast(ray.Origin, ray.Direction * (Config.MaxRange or 500), raycastParams)

	if result then
		return result.Position
	end

	return ray.Origin + ray.Direction * (Config.MaxRange or 500)
end

local function playShootSound()
	local sound = ShootSound:Clone()
	sound.Parent = Tool.Handle
	if not sound.IsLoaded then
		sound.Loaded:Wait()
	end
	sound:Play()
	Debris:AddItem(sound, sound.TimeLength + 0.1)
end

local function shootBulletEffect(startPos: Vector3, endPos: Vector3)
	local attachment0 = Instance.new("Attachment")
	attachment0.WorldCFrame = CFrame.new(startPos)
	attachment0.Parent = workspace

	local attachment1 = Instance.new("Attachment")
	attachment1.WorldCFrame = CFrame.new(endPos)
	attachment1.Parent = workspace

	local beam = Instance.new("Beam")
	beam.Attachment0 = attachment0
	beam.Attachment1 = attachment1
	beam.Width0 = 0.1
	beam.Width1 = 0.1
	beam.LightEmission = 1
	beam.LightInfluence = 0
	beam.FaceCamera = true
	beam.Color = ColorSequence.new({
		ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 255, 0)),
		ColorSequenceKeypoint.new(1, Color3.fromRGB(255, 200, 100)),
	})
	beam.Parent = Camera

	task.wait()

	local tweenInfo = TweenInfo.new(0.05, Enum.EasingStyle.Linear)

	local tween1 = TweenService:Create(beam, tweenInfo, { Width0 = 0 })
	tween1:Play()
	tween1.Completed:Wait()

	local tween2 = TweenService:Create(beam, tweenInfo, { Width1 = 0 })
	tween2:Play()
	tween2.Completed:Wait()

	beam:Destroy()
	attachment0:Destroy()
	attachment1:Destroy()
end

local function fire()
	if not canFire then return end
	if not isEquipped then return end

	local localCharacter = LocalPlayer.Character
	if not localCharacter then return end

	local localHumanoid = localCharacter:FindFirstChildOfClass("Humanoid")
	if not localHumanoid or localHumanoid.Health <= 0 then return end

	canFire = false
	updateCursor()

	local targetPosition = getTargetPosition()

	playShootSound()
	FireGunRemote:FireServer(targetPosition)

	local handle = Tool:FindFirstChild("Handle")
	local startPos = handle and handle.Position or localCharacter.HumanoidRootPart.Position
	task.spawn(shootBulletEffect, startPos, targetPosition)

	task.delay(FIRE_COOLDOWN, function()
		canFire = true
		updateCursor()
	end)
end

Tool.Equipped:Connect(function(m)
	mouse = m
	isEquipped = true
	updateCursor()
end)

Tool.Unequipped:Connect(function()
	isEquipped = false
	if mouse then
		mouse.Icon = ""
	end
	mouse = nil
end)

Tool.Activated:Connect(function()
	fire()
end)

GunHitRemote.OnClientEvent:Connect(function()
end)

PlayerEliminatedRemote.OnClientEvent:Connect(function(killer: Player, victim: Player)
	if victim == LocalPlayer then
		canFire = false
		isEquipped = false
	end
end)

Any help is appreciated

1 Like

ok so If you spawn/move a physical part on the client, roblox may give that part client something i like to call “network ownership”. basically the server and other cliens will still simulate the part and can sorta “fight” over its position that causes freezes and jitters. Turning cancollide=false prevents pysics colisions, but if you still use physics (velocities, bodymovers, etc) the part can interact with other consraints or be replicated oddly so the server/client conflict can look like a bounce!

The fix is: dont rely on physics for the auhoriative hit detection of fast projeciles. Use serverside raycasts and move visuals separately :grinning_face_with_smiling_eyes:

1 Like