Bullet Raycast Problem

Hello everyone, I have a weapon system, but I’m having some issues with the mobile firing system. The main problem is this: the bullet should travel straight in the direction of the cursor, but it always goes downwards and hits the ground near my feet instead of going to the cursor’s location. I’m aiming the cursor at NPCs, but the bullet doesn’t even reach them. I really need someone who can help me solve this problem, as it’s significantly slowing down the game development process. I would appreciate it if someone familiar with the raycast system could assist me.






client module

--[[
	Created by devsydiens
]]

local gunClient = {}

local inputService = game:GetService("UserInputService")
local tweenService = game:GetService("TweenService")
local us = game.ReplicatedStorage:FindFirstChild("stroke")
local ust = game.ReplicatedStorage:FindFirstChild("strokee")

local player = game:GetService("Players").LocalPlayer
local mouse = player:GetMouse()
local camera = workspace.CurrentCamera

local bulletModule = require(player.PlayerScripts:WaitForChild("bulletRender"))
local gunGui = script:WaitForChild("gunGui")

local totalFired = 0

-- Mobil control
local isMobile = inputService.TouchEnabled and not inputService.MouseEnabled
local mobileCursorGui = nil
local mobileControlsGui = nil

--\\ Mobil cursor
local function ensureMobileCursor()
	if mobileCursorGui and mobileCursorGui.Parent then 
		return mobileCursorGui 
	end

	local oldGui = player.PlayerGui:FindFirstChild("MobileCrosshair")
	if oldGui then oldGui:Destroy() end

	local screenGui = Instance.new("ScreenGui")
	screenGui.Name = "MobileCrosshair"
	screenGui.ResetOnSpawn = false
	screenGui.DisplayOrder = 100
	screenGui.IgnoreGuiInset = true

	local cursor = Instance.new("Frame")
	cursor.Name = "Cursor"
	cursor.Size = UDim2.new(0, 4, 0, 4)
	cursor.Position = UDim2.new(0.5, -2, 0.5, -2)
	cursor.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
	cursor.BorderSizePixel = 0
	cursor.Parent = screenGui

	local line1 = Instance.new("Frame")
	line1.Size = UDim2.new(0, 20, 0, 2)
	line1.Position = UDim2.new(0.5, -10, 0.5, -1)
	line1.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
	line1.BorderSizePixel = 0
	line1.Parent = screenGui

	local line2 = Instance.new("Frame")
	line2.Size = UDim2.new(0, 2, 0, 20)
	line2.Position = UDim2.new(0.5, -1, 0.5, -10)
	line2.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
	line2.BorderSizePixel = 0
	line2.Parent = screenGui

	screenGui.Parent = player.PlayerGui
	mobileCursorGui = screenGui

	return screenGui
end

--\\ Mobil buttons
local function createMobileButtons(fireCallback, reloadCallback)
	if mobileControlsGui then
		mobileControlsGui:Destroy()
	end

	local screenGui = Instance.new("ScreenGui")
	screenGui.Name = "MobileGunControls"
	screenGui.ResetOnSpawn = false
	screenGui.DisplayOrder = 50
	screenGui.IgnoreGuiInset = true

	local fireButton = Instance.new("TextButton")
	fireButton.Name = "FireButton"
	fireButton.Size = UDim2.new(0, 60, 0, 60)
	fireButton.Position = UDim2.new(1, -120, 1, -160)
	fireButton.BackgroundColor3 = Color3.fromRGB(70, 70, 70)
	fireButton.BorderSizePixel = 0
	fireButton.Text = "Fire"
	fireButton.BackgroundTransparency = 0.3
	fireButton.TextSize = 40
	fireButton.TextScaled = true
	fireButton.TextColor3 = Color3.new(1, 1, 1)
	fireButton.Font = Enum.Font.GothamBold
	fireButton.AutoButtonColor = false
	fireButton.Parent = screenGui
	if ust then ust:Clone().Parent = fireButton end

	local fireCorner = Instance.new("UICorner")
	fireCorner.CornerRadius = UDim.new(0, 40)
	fireCorner.Parent = fireButton

	local reloadButton = Instance.new("TextButton")
	reloadButton.Name = "ReloadButton"
	reloadButton.Size = UDim2.new(0, 60, 0, 60)
	reloadButton.Position = UDim2.new(0, 20, 1, -160)
	reloadButton.BackgroundColor3 = Color3.fromRGB(70, 70, 70)
	reloadButton.BorderSizePixel = 0
	reloadButton.Text = "Reload"
	reloadButton.TextSize = 35
	reloadButton.TextScaled = true
	reloadButton.TextColor3 = Color3.new(1, 1, 1)
	reloadButton.BackgroundTransparency = 0.3
	reloadButton.Font = Enum.Font.GothamBold
	reloadButton.AutoButtonColor = false
	reloadButton.Parent = screenGui
	if us then us:Clone().Parent = reloadButton end

	local reloadCorner = Instance.new("UICorner")
	reloadCorner.CornerRadius = UDim.new(0, 40)
	reloadCorner.Parent = reloadButton

	fireButton.MouseButton1Down:Connect(function()
		fireButton.BackgroundColor3 = Color3.fromRGB(33, 33, 33)
		fireCallback(true)
	end)

	fireButton.MouseButton1Up:Connect(function()
		fireButton.BackgroundColor3 = Color3.fromRGB(70, 70, 70)
		fireCallback(false)
	end)

	reloadButton.MouseButton1Click:Connect(function()
		reloadButton.BackgroundColor3 = Color3.fromRGB(33, 33, 33)
		reloadCallback()
		wait(0.2)
		reloadButton.BackgroundColor3 = Color3.fromRGB(70, 70, 70)
	end)

	screenGui.Parent = player.PlayerGui
	mobileControlsGui = screenGui

	return screenGui
end

function gunClient.init(tool)
	repeat wait() until player.Character and player.Character.Parent == workspace; wait(.25)

	local character = player.Character
	local humanoid = character:WaitForChild("Humanoid")
	local root = character:WaitForChild("HumanoidRootPart")

	local emptySound = tool:WaitForChild("Handle"):WaitForChild("emptySound")
	local reloadSound = tool:WaitForChild("Handle"):WaitForChild("reloadSound")
	local mag = tool:WaitForChild("GunModel"):WaitForChild("Mag")

	local bulletStorage = workspace:WaitForChild("bulletStorage")
	local gunStorage = game:GetService("ReplicatedStorage"):WaitForChild("gunStorage")
	local gunEvents = gunStorage:WaitForChild("events")
	local fireEvent = gunEvents:WaitForChild("fire")
	local reloadEvent = gunEvents:WaitForChild("reload")
	local posVerifier = gunEvents:WaitForChild("positionVerifier")

	local config = tool:WaitForChild("config")
	local animations = config:WaitForChild("animations")
	local mainConfigs = config:WaitForChild("main")
	local miscConfgs = config:WaitForChild("misc")
	local modeConfigs = config:WaitForChild("mode")

	local settings = tool:FindFirstChild("settings")
	local CAMERA_SHAKE_INTENSITY = settings and settings:FindFirstChild("cameraShake") and settings.cameraShake.Value or 1.5
	local BLUR_SIZE = settings and settings:FindFirstChild("blurSize") and settings.blurSize.Value or 8

	local shotgun = mainConfigs.shotgun.Value
	local pellets = mainConfigs.shotgun.pellets.Value
	local rpm = mainConfigs.rpm.Value
	local spread = mainConfigs:WaitForChild("spread").Value
	local ammo = tool:WaitForChild("ammo")

	local totalAmmoValue = tool:FindFirstChild("totalAmmo")
	if not totalAmmoValue then
		totalAmmoValue = Instance.new("IntValue")
		totalAmmoValue.Name = "totalAmmo"
		local magazineSize = mainConfigs.ammo.Value
		local magazineCount = mainConfigs:FindFirstChild("magazineCount") and mainConfigs.magazineCount.Value or 2
		totalAmmoValue.Value = magazineSize * magazineCount
		totalAmmoValue.Parent = tool
	end

	local aimAnim = humanoid:LoadAnimation(animations:WaitForChild("aimAnim"))
	local holsterAnim = humanoid:LoadAnimation(animations:WaitForChild("holsterAnim"))
	local reloadAnim = humanoid:LoadAnimation(animations:WaitForChild("reloadAnim"))
	local recoilAnim = humanoid:LoadAnimation(animations:WaitForChild("recoilAnim"))

	local equipAnim
	if animations:FindFirstChild("equipAnim") then
		equipAnim = humanoid:LoadAnimation(animations:FindFirstChild("equipAnim"))
		equipAnim.Priority = Enum.AnimationPriority.Action4
	end

	local hAnim = false
	local canFire = true
	local equipped = false
	local mouseDown = false
	local reloading = false
	local canRespond = false
	local gunMode

	local blur = Instance.new("BlurEffect")
	blur.Size = 0
	blur.Parent = camera

	for _,v in pairs(modeConfigs:GetChildren()) do
		if v.Value then
			gunMode = v.Name
		end
	end

	mouse.TargetFilter = workspace.bulletStorage

	aimAnim.Priority = Enum.AnimationPriority.Action
	holsterAnim.Priority = Enum.AnimationPriority.Action
	recoilAnim.Priority = Enum.AnimationPriority.Action
	reloadAnim.Priority = Enum.AnimationPriority.Action

	local function cameraShake()
		spawn(function()
			for i = 1, 3 do
				local randomOffset = CFrame.Angles(
					math.rad(math.random(-CAMERA_SHAKE_INTENSITY, CAMERA_SHAKE_INTENSITY)),
					math.rad(math.random(-CAMERA_SHAKE_INTENSITY, CAMERA_SHAKE_INTENSITY)),
					math.rad(math.random(-CAMERA_SHAKE_INTENSITY, CAMERA_SHAKE_INTENSITY))
				)
				camera.CFrame = camera.CFrame * randomOffset
				wait(0.02)
			end
		end)

		spawn(function()
			tweenService:Create(blur, TweenInfo.new(0.05), {Size = BLUR_SIZE}):Play()
			wait(0.05)
			tweenService:Create(blur, TweenInfo.new(0.15), {Size = 0}):Play()
		end)
	end

	local ammoListen
	local totalAmmoListen
	local currentGui

	local function updateGUI()
		if currentGui and currentGui.Parent then
			currentGui.infoFrame.gunAmmo.Text = tostring(ammo.Value).." / "..tostring(totalAmmoValue.Value)
		end
	end

	local function reload()
		if not reloading and ammo.Value < mainConfigs.ammo.Value and totalAmmoValue.Value > 0 then
			reloading = true
			mag.Transparency = 1

			reloadAnim:Play(.1, 1, reloadAnim.Length / mainConfigs.reloadTime.Value)

			local maxAmmo = mainConfigs.ammo.Value
			local neededAmmo = maxAmmo - ammo.Value
			local ammoToLoad = math.min(neededAmmo, totalAmmoValue.Value)

			reloadEvent:FireServer(tool.Name, ammoToLoad)

			local canStop = true
			local unequipListen
			unequipListen = tool.Unequipped:Connect(function()
				unequipListen:Disconnect()
				canStop = false
			end)

			wait(mainConfigs.reloadTime.Value)

			unequipListen:Disconnect()
			if canStop then
				ammo.Value = ammo.Value + ammoToLoad
				totalAmmoValue.Value = totalAmmoValue.Value - ammoToLoad
				updateGUI()
				reloading = false
				mag.Transparency = 0
			end
		end
	end

	-- ════════════════════════════════════════════════════════════
	-- mouse hit
	-- ════════════════════════════════════════════════════════════

	
	local function getMouseHit()
		-- PC mouse.Hit
		if not isMobile then
			return mouse.Hit.Position
		end

		-- Mobilde raycast
		local screenCenter = Vector2.new(camera.ViewportSize.X / 2, camera.ViewportSize.Y / 2)
		local unitRay = camera:ScreenPointToRay(screenCenter.X, screenCenter.Y)

		-- Ray oluştur
		local origin = unitRay.Origin
		local direction = unitRay.Direction * 1000

		-- Raycast
		local ray = Ray.new(origin, direction)
		local hitPart, hitPos = workspace:FindPartOnRayWithIgnoreList(ray, {character, bulletStorage}, false, true)

		
		if hitPos then
			return hitPos
		else
			return origin + direction
		end
	end

	local function fire()
		if not equipped or not canFire or humanoid.Health <= 0 or reloading then
			return
		end

		if ammo.Value <= 0 then
			emptySound:Play()
			return
		end

	
		local ray = Ray.new(root.CFrame.p, (tool.barrel.CFrame.p - root.CFrame.p).unit * (root.Position - tool.barrel.Position).magnitude)
		local part = workspace:FindPartOnRayWithIgnoreList(ray, {character, bulletStorage}, false, true)

		if not part or part.Parent:FindFirstChildOfClass("Humanoid") or part.Parent.Parent:FindFirstChildOfClass("Humanoid") then
			-- take the position
			local targetPos = getMouseHit()

			if shotgun then
				spawn(function()
					for i = 1, pellets do
						
						local endPos = targetPos + Vector3.new(
							math.random(-spread * 10, spread * 10) / 10,
							math.random(-spread * 10, spread * 10) / 10,
							math.random(-spread * 10, spread * 10) / 10
						)

						bulletModule.renderBullet(player, tool.Name, endPos)

						if i >= pellets then
							fireEvent:FireServer(tool.Name, endPos, true)
						else
							fireEvent:FireServer(tool.Name, endPos, false)
							totalFired += 1
						end
					end
				end)
			else
				
				local endPos = targetPos + Vector3.new(
					math.random(-spread * 10, spread * 10) / 10,
					math.random(-spread * 10, spread * 10) / 10,
					math.random(-spread * 10, spread * 10) / 10
				)

				fireEvent:FireServer(tool.Name, endPos)
				totalFired += 1

				bulletModule.renderBullet(player, tool.Name, endPos)
			end

			local totalWhenFired = totalFired

			canFire = false
			delay(60 / rpm, function()
				canFire = true
			end)

			local canRespond = true
			posVerifier.OnClientInvoke = function()
				if canRespond then
					canRespond = false
					local response = player.UserId * player.UserId - player.UserId
					response = response + #game:GetService("Players"):GetPlayers()
					return response, totalWhenFired
				end
			end

			recoilAnim:Play()
			cameraShake()
		end
	end

	-- ════════════════════════════════════════════════════════════
	-- fire system ending
	-- ════════════════════════════════════════════════════════════

	local function mouseBegan()
		if gunMode == "auto" then
			while mouseDown and equipped do
				if canFire then
					fire()
				end
				wait(60 / rpm)
			end
		elseif gunMode == "semi" then
			if canFire and equipped then
				fire()
			end
		end
	end

	tool.Equipped:Connect(function()
		if equipAnim then
			equipAnim:Play()
			wait(equipAnim.Length * 0.5)
		end

		aimAnim:Play()

		if isMobile then
			local cursor = ensureMobileCursor()
			if cursor then
				cursor.Enabled = true
			end

			createMobileButtons(
				function(isPressed)
					if isPressed then
						mouseDown = true
						spawn(mouseBegan)
					else
						mouseDown = false
					end
				end,
				function()
					spawn(reload)
				end
			)
		else
			mouse.Icon = "rbxassetid://"..config.misc.crosshair.Value
		end

		equipped = true

		currentGui = gunGui:Clone()
		currentGui.Parent = player.PlayerGui

		currentGui.infoFrame.gunName.Text = tool.Name
		updateGUI()

		ammoListen = ammo.Changed:Connect(updateGUI)
		totalAmmoListen = totalAmmoValue.Changed:Connect(updateGUI)
	end)

	tool.Unequipped:Connect(function()
		holsterAnim:Stop()
		aimAnim:Stop()
		reloadAnim:Stop()
		recoilAnim:Stop()
		mag.Transparency = 0
		if equipAnim then
			equipAnim:Stop()
		end

		blur.Size = 0

		if mobileCursorGui then
			mobileCursorGui.Enabled = false
		end

		if mobileControlsGui then
			mobileControlsGui:Destroy()
			mobileControlsGui = nil
		end

		if currentGui then
			currentGui:Destroy()
			currentGui = nil
		end
		if ammoListen then
			ammoListen:Disconnect()
			ammoListen = nil
		end
		if totalAmmoListen then
			totalAmmoListen:Disconnect()
			totalAmmoListen = nil
		end

		equipped = false
		mouseDown = false
		reloading = false
		mouse.Icon = ""
	end)

	inputService.InputBegan:Connect(function(input, gpe)
		if equipped and not gpe then
			if input.UserInputType == Enum.UserInputType.MouseButton1 then
				mouseDown = true
				mouseBegan()
			elseif input.UserInputType == Enum.UserInputType.Keyboard then
				if input.KeyCode == Enum.KeyCode.R then
					if not reloading then
						reload()
					end
				elseif input.KeyCode == Enum.KeyCode.F then
					if not hAnim then
						hAnim = true
						holsterAnim:Play()
					else
						hAnim = false
						holsterAnim:Stop()
					end
				end
			end
		end
	end)

	inputService.InputEnded:Connect(function(input, gpe)
		if input.UserInputType == Enum.UserInputType.MouseButton1 then
			mouseDown = false
		end
	end)
end

return gunClient

bullet render code


local bulletModule = {}

local player = game:GetService("Players").LocalPlayer
local tweenService = game:GetService("TweenService")
local debrisService = game:GetService("Debris")

local bulletStorage = workspace:WaitForChild("bulletStorage")
local gunStorage = game:GetService("ReplicatedStorage"):WaitForChild("gunStorage")

local gunEvents = gunStorage:WaitForChild("events")
local fireEvent = gunEvents:WaitForChild("fire")
local damageEvent = gunEvents:WaitForChild("damage")

local gunObjects = gunStorage:WaitForChild("objects")
local fleshTemp = gunObjects:WaitForChild("fleshHit")
local objectTemp = gunObjects:WaitForChild("objectHit")
local hitmarker = game:GetService("SoundService"):WaitForChild("hitmarker")

-- Mermi ayarları
local BULLET_SPEED = 1000 
local BULLET_LENGTH = 2 
local BULLET_WIDTH = 0.2 

--\\ functions
local function getCallersGun(caller, gunName)
	local tool = caller.Character:FindFirstChildOfClass("Tool")

	if tool and tool:FindFirstChild("gunClient") and tool.Name == gunName then
		return tool
	else
		for _,v in pairs(caller.Backpack:GetChildren()) do
			if v:IsA("Tool") and v:FindFirstChild("gunClient") and v.Name == gunName then
				return v
			end
		end
	end
end

local function burstCosmetics(object, length)
	for _,v in pairs(object:GetChildren()) do
		if v:IsA("ParticleEmitter") or v:IsA("Light") then
			v.Enabled = true

			delay(length, function()
				v.Enabled = false
			end)
		end
	end
end

local function firesound(object, lenght)
	for _,b in pairs(object:GetChildren()) do
		if b:IsA("Sound") then
			b:Play()					
		end
	end 
end



function bulletModule.renderBullet(caller, gunName, endPos)
	local gun = getCallersGun(caller, gunName)

	if gun ~= nil then
		local barrel = gun.barrel
		local handle = gun.Handle
		
		if player.Character then
			local root = player.Character:FindFirstChild("HumanoidRootPart")

			if root and (root.Position - barrel.Position).magnitude < 750 then
				burstCosmetics(barrel)
				firesound(handle)
				-- Raycast ile hedef bul
				local ray = Ray.new(barrel.Position, (endPos - barrel.Position).Unit * 5000)

				local hitPart, hitPos, hitNorm = workspace:FindPartOnRayWithIgnoreList(ray, {caller.Character, bulletStorage}, false, true)

				local startPos = barrel.Position
				local targetPos = hitPos or endPos
				local direction = (targetPos - startPos).Unit
				local distance = (targetPos - startPos).Magnitude

				
				local bullet = Instance.new("Part")
				bullet.Name = "Bullet"
				bullet.Anchored = true
				bullet.CanCollide = false
				bullet.Transparency = 1
				bullet.Size = Vector3.new(BULLET_WIDTH, BULLET_WIDTH, BULLET_LENGTH)
				bullet.CFrame = CFrame.new(startPos, targetPos)
				bullet.Color = Color3.fromRGB(255, 240, 100)
				bullet.Material = Enum.Material.Neon
				bullet.Parent = bulletStorage
			
				
				local travelTime = distance / BULLET_SPEED
				local startTime = tick()

				local heartbeatConnection
				heartbeatConnection = game:GetService("RunService").Heartbeat:Connect(function()
					local elapsed = tick() - startTime
					local alpha = math.min(elapsed / travelTime, 1)

					if alpha >= 1 then
						-- Hedefe ulaştı
						heartbeatConnection:Disconnect()
						bullet:Destroy()
					else
						
						local currentPos = startPos + (direction * distance * alpha)
						bullet.CFrame = CFrame.new(currentPos, currentPos + direction)
						
					end
				end)

			    --anti
				debrisService:AddItem(bullet, travelTime + 0.2)

				
				if hitPart ~= nil and hitPart.Parent ~= nil then
					local humanoid = hitPart.Parent:FindFirstChildOfClass("Humanoid") or hitPart.Parent.Parent:FindFirstChildOfClass("Humanoid")

					if humanoid then
						if player == caller then
							hitmarker:Play()
							damageEvent:FireServer(gunName, hitPart)
						end

						local fleshHit = fleshTemp:Clone()
						fleshHit.CFrame = CFrame.new(hitPos, hitPos + hitNorm)
						fleshHit.Parent = bulletStorage
						burstCosmetics(fleshHit, .15)
						debrisService:AddItem(fleshHit, .15)
					else
						local objectHit = objectTemp:Clone()
						objectHit.CFrame = CFrame.new(hitPos, hitPos + hitNorm)
						objectHit.Parent = bulletStorage
						burstCosmetics(objectHit, .15)
						debrisService:AddItem(objectHit, .15)
					end
				end
			end
		end
	end
end

--\\ returning module
return bulletModule

Hi, the issue is that the gunClient module uses ScreenPointToRay instead of ViewportPointToRay. ScreenPointToRay accounts for core gui inset, so it effectively pushes the entire viewing space down. Instead, use ViewportPointToRay, since it uses the “true” screen position, ignoring inset.

Also beware that

is deprecated, workspace:Raycast() would be more appropriate