HitScan bullets NOT working, feels like shooting from a mile away

Hey everyone,

I’m working on a hitscan pistol in Roblox and have an issue: currently, my bullets don’t always hit exactly where my crosshair is aimed, especially when targets move sideways. It feels like there’s aim prediction or leading applied, which makes shooting feel unnatural.

  • When I shoot, the bullet should hit exactly what’s under my crosshair at the moment of firing.
  • No aiming ahead of moving targets (no prediction or aim assist).
  • The bullet is hitscan, so it should be instant and precise.

Here’s my server script with slight modifications :

Minimalized Server Script

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

local shotEvent = ReplicatedStorage:WaitForChild("Guns"):WaitForChild("Weapons"):WaitForChild("Pistol"):WaitForChild("ShotClient")
local Pistol = require(ReplicatedStorage.Guns.Weapons.Pistol.PistolModule)

local GoldenPistols = {}

local MAX_RANGE = 300

Players.PlayerAdded:Connect(function(player)
    player.CharacterAdded:Connect(function(character)
        local pistolTool = character:WaitForChild("Pistol", 10)
        if not pistolTool then
            warn("Player "..player.Name.." missing pistol")
            return
        end

        local pistol = Pistol.new(character)
        GoldenPistols[player.UserId] = pistol

        local humanoid = character:WaitForChild("Humanoid")
        humanoid.Died:Connect(function()
            GoldenPistols[player.UserId] = nil
        end)
    end)
end)

Players.PlayerRemoving:Connect(function(player)
    GoldenPistols[player.UserId] = nil
end)

shotEvent.OnServerEvent:Connect(function(player, origin, targetPos)
    if typeof(origin) ~= "Vector3" or typeof(targetPos) ~= "Vector3" then
        warn("Invalid shot data from "..player.Name)
        return
    end

    local pistol = GoldenPistols[player.UserId]
    if not pistol then
        warn("No pistol instance for "..player.Name)
        return
    end

    local direction = (targetPos - origin)
    local distance = math.min(direction.Magnitude, MAX_RANGE)
    direction = direction.Unit

    local params = RaycastParams.new()
    params.FilterDescendantsInstances = {player.Character, pistol.Handle.Parent}
    params.FilterType = Enum.RaycastFilterType.Exclude
    params.IgnoreWater = true

    local raycastResult = workspace:Raycast(origin, direction * distance, params)

    if raycastResult then
        pistol:ProcessHit(player, raycastResult)
    else
        -- No hit, but still process firing if needed
        pistol:ProcessHit(player, nil)
    end
end)
Minimalized Client Script
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local player = Players.LocalPlayer
local camera = workspace.CurrentCamera

local shotEvent = ReplicatedStorage:WaitForChild("Guns"):WaitForChild("Weapons"):WaitForChild("Pistol"):WaitForChild("ShotClient")

local mouse = player:GetMouse()

-- Example firing function, called when player clicks or shoots
local function fire()
    if not mouse.Hit then return end

    local origin = camera.CFrame.Position
    local targetPos = mouse.Hit.p

    shotEvent:FireServer(origin, targetPos)
end

-- Bind to left click or your input method
mouse.Button1Down:Connect(fire)

On the server, I raycast from the origin towards the targetPos, clamping the distance and filtering out the shooter’s character. The problem is that even though I’m sending exactly where the player’s crosshair is pointing, shots often don’t register exactly where I’m aiming, especially when players are moving sideways or strafing.

The bullets are hitscan, and I’m not doing any prediction or leading targets. Still, the shot detection feels off, and I want the bullets to hit precisely when the enemy is under the crosshair — no needing to “lead” or guess their movement.

What am I missing here?

  • Is it a server/client latency issue?
  • Is my raycast origin or direction off by a little due to camera FOV or position?
  • Should I be using a different method to get the exact crosshair position in 3D space?
  • Any best practices for syncing hitscan weapons so that the client’s crosshair matches server hit detection perfectly?

I’d appreciate any advice or suggestions on how to fix this issue and make the hits feel fair and accurate!

Thanks in advance.
( just saying, normally it connects to another module script and uses that for raycasting, the server just handles the events and connecting stuff :slight_smile: )

2 Likes

There is a delay when sending signals to the server. It won’t be completely precise.
You should opt for getting the raycast result in the client and sending that to the server along the origin and direction arguments.
→ On the server, you want to first verify the legitimacy of the raycast result being passed in by the client. For this, the server should cast it’s own ray using the origin and direction values provided, and comparing the position of the raycast result done on the server vs the raycast result done on the client, using magnitude checks.
→ After all is set and done, the server can call invoke ProcessHit on the raycast result sent by the client, if it was verified and is legit.
→ If its not legit, then simply don’t process anything. I don’t recommend outright punishing the player for suspicion of exploits since internet speed can vary a lot.

1 Like

Well, here’s my current setup :slight_smile: :

I get first get the raycast on the client and send it to the server :

	elseif fireMode == "Semi" then
			if not lastMouseState and now - lastShot >= Cooldown then

				lastShot = now
				local origin = camera.CFrame.Position
				local mouseHitPos = mouse.Hit and mouse.Hit.p or (origin + (camera.CFrame.LookVector * 500))
				local direction = (mouseHitPos - origin).Unit

				local raycastParams = RaycastParams.new()
				raycastParams.FilterDescendantsInstances = {player.Character, tool}
				raycastParams.FilterType = Enum.RaycastFilterType.Exclude
				raycastParams.IgnoreWater = true

				local raycastResult = workspace:Raycast(origin, direction * maxDistance, raycastParams)

				if raycastResult then
					shotEvent:FireServer(raycastResult.Position, raycastResult.Instance, camera.CFrame.Position, camera.CFrame.RightVector)
				else
					shotEvent:FireServer(origin + direction * maxDistance, nil, camera.CFrame.Position)
				end
			end
		end

Then, on the server I use the module script to create a new pistol instance and then use my :Fired method to cast the global ray for the pistol :

shotEvent.OnServerEvent:Connect(function(player, mouseHitPosition, hitInstance, camera, rightVector)
	local pistol = GoldenPistols[player.UserId]
	if pistol then
		pistol:Fired(player, mouseHitPosition, camera, rightVector)
	else
		warn("No pistol instance found for player "..player.Name)
	end
end)

Module script snippet :

function Pistol:Fired(player, mouseHit, camera, rightVector)
	-- Validate metatable safely
	if getmetatable(self) ~= Pistol then
		warn("Pistol:Fired called on invalid object")
		return
	end

	if self.Debounce then return end
	if tick() - self.MainTick < self.Cooldown then return end

	-- Validate player and character presence
	if not player or not player.Character or not player.Character:FindFirstChild("HumanoidRootPart") then
		warn("Invalid player or character in Pistol:Fired")
		return
	end

	-- Validate Fire part
	if not self.Fire or not self.Fire:IsA("BasePart") then
		warn("Invalid Fire part in Pistol:Fired")
		return
	end

	-- Clamp mouseHit within MAX_RANGE from Fire.Position
	local origin = camera

	local toTarget = mouseHit - origin
	if toTarget.Magnitude > MAX_RANGE then
		mouseHit = origin + toTarget.Unit * MAX_RANGE
	end

	self.Debounce = true
	self.MainTick = tick()

	-- Setup raycast params excluding player character and tool handle
	local params = RaycastParams.new()
	params.FilterType = Enum.RaycastFilterType.Exclude
	params.FilterDescendantsInstances = {player.Character, self.Handle.Parent}
	params.IgnoreWater = true

	local hit = workspace:Raycast(origin, (mouseHit - origin).Unit * MAX_RANGE, params)
	local hitPos = hit and hit.Position or mouseHit
	local dist = (hitPos - origin).Magnitude

	-- Beam visual
	local beam = ReplicatedStorage.Guns.LaserBeam:Clone()
	beam.Size = Vector3.new(0.05, 0.05, dist)
	beam.CFrame = CFrame.new(origin, hitPos)
	beam.EndAttachment.Position = Vector3.new(0, 0, -dist)
	beam.Anchored = true
	beam.CanCollide = false
	beam.Parent = workspace
	TweenService:Create(beam.StartAttachment, TweenInfo.new(0.1), {Position = beam.EndAttachment.Position}):Play()
	game.Debris:AddItem(beam, 0.1)

With this setup, the latency stuff occurs. I just wanted to clarify this, I’ll try your method out ASAP although it is almost the same.

the problem’s still the same, i need support :sweat_smile:

Actually, it worked. Rather than raycasting twice, using the client’s raycast and verifying it was the very key. Thanks a BUNCH. :slight_smile:

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.