How can I improve this raycast system?

Hey developers!
I’ve just finished making my weapon using raycasting, it’s not very good but it’s a start. It has a lot of bugs in it (not shooting where the mouse is if the camera is at a certain angle) and is not how I would imagine games like Arsenal make weapons, does anyone know how I could improve this code to be more efficient, and how big games make weapon systems?

Client:
local player = game.Players.LocalPlayer
local UIS = game:GetService("UserInputService")
local mouse = player:GetMouse()
local playerGui = player:WaitForChild("PlayerGui")

local tool = script.Parent

local runService = game:GetService("RunService")
local camera = workspace.CurrentCamera

local shoot_event = tool:WaitForChild("Shoot")
local reload_event = tool:WaitForChild("Reload")
local player_killed_event = tool:WaitForChild("PlayerKilled")
local hit_player = tool:WaitForChild("HitPlayer")

local hold_anim = tool:WaitForChild("Hold")
local hold_track = player.Character:WaitForChild("Humanoid").Animator:LoadAnimation(hold_anim)

local ammoUI = tool:WaitForChild("AmmoUI")
local player_killedUI = playerGui:WaitForChild("PlayerKilled")
local hitmarkerEffect = ammoUI:WaitForChild("Hitmarker")

local hitmarkerSound = tool:WaitForChild("Hitmarker")

local bullet = game.ReplicatedStorage:WaitForChild("Bullet")

local reloading = false

local maxAmmo = 30
local currentAmmo = maxAmmo

local mouseDown = false

local function textEffect(textLabel, operator)
	local method
	spawn(function()
		for index = 1, 20 do
			if operator == "-" then
				textLabel.TextTransparency -= 0.05
				task.wait(.01)
			else
				textLabel.TextTransparency += 0.05
				task.wait(.01)
			end
		end
	end)
end

local function _rayHitPlayer()
	hitmarkerSound:Play()
	spawn(function()
		hitmarkerEffect.Visible = true
		task.wait(.01)
		hitmarkerEffect.Visible = false
	end)
	textEffect()
end

local function castRay(mousePos)
	shoot_event:FireServer("nil")
	local origin = tool:WaitForChild("ShootPart").Position
	local direction = (mousePos - origin).Unit
	local rayInfo = RaycastParams.new()
	rayInfo.FilterDescendantsInstances = {player.Character, tool}
	rayInfo.FilterType = Enum.RaycastFilterType.Blacklist
	
	local result = workspace:Raycast(origin, direction * 500, rayInfo)
	local inter = result and result.Position or (direction * 500) - origin
	local distanceTravel = (origin - inter).Magnitude
	
	local bulletClone = bullet:Clone()
	bulletClone.Size = Vector3.new(0.1, 0.1, distanceTravel)
	bulletClone.CFrame = CFrame.new(origin, inter) * CFrame.new(0, 0, -distanceTravel / 2)
	bulletClone.Parent = workspace
	
	if result then
		local instancePart = result.Instance
		if instancePart then
			if instancePart.Parent then
				if instancePart.Parent:FindFirstChild("Humanoid") or instancePart.Parent.Parent:FindFirstChild("Humanoid") then
					local humanoid = instancePart.Parent:FindFirstChild("Humanoid") or instancePart.Parent.Parent:FindFirstChild("Humanoid")
					_rayHitPlayer()
					if humanoid.Health > 0 then
						shoot_event:FireServer(humanoid)
					end
				end
			end
		end
	end
	task.wait(.001)
	bulletClone:Destroy()
end

local function fireBullet()
	while mouseDown == true do
		camera.CFrame = camera.CFrame * CFrame.Angles(math.pi / 100, 0, 0)
		currentAmmo -= 1
		castRay(mouse.Hit.Position)
		wait(0.09)
	end
end


mouse.Button1Down:Connect(function()
	mouseDown = true
end)

mouse.Button1Up:Connect(function()
	mouseDown = false
end)

tool.Equipped:Connect(function()
	ammoUI.Parent = player.PlayerGui
	hold_track.Looped = true
	hold_track:Play()
end)

tool.Unequipped:Connect(function()
	ammoUI.Parent = tool
	hold_track.Looped = false
	hold_track:Stop()
end)

mouse.Button1Down:Connect(function()
	if player.Character:FindFirstChild(tool.Name) and reloading == false and currentAmmo > 0 and player.Character.Humanoid.Health > 0 then
		camera.CFrame = camera.CFrame * CFrame.Angles(math.pi / 100, 0, 0)
		currentAmmo -= 1
		castRay(mouse.Hit.Position)
		wait(0.09)
		fireBullet()
	end
end)

player_killed_event.OnClientEvent:Connect(function(playerName)
	player_killedUI.Back.PlayerName.Text = playerName
	local function playerKillEffect(operator)
		textEffect(player_killedUI.Back.PlayerName, operator)
		textEffect(player_killedUI.Back.Title, operator)
		textEffect(player_killedUI.Back.XP, operator)
	end
	player_killedUI.Back.Visible = true
	playerKillEffect("-")
	task.wait(3)
	playerKillEffect("+")
	player_killedUI.Back.Visible = false
end)

Server:
local tool = script.Parent

local shoot_event = tool:WaitForChild("Shoot")
local reload_event = tool:WaitForChild("Reload")
local player_killed_event = tool:WaitForChild("PlayerKilled")
local hit_player = tool:WaitForChild("HitPlayer")

local shoot_sound = tool:WaitForChild("Handle"):WaitForChild("Fire")

local function shootEffect()
	shoot_sound:Play()
	spawn(function()
		tool.Flash.lite.Enabled = true
		tool.Flash.Light.Enabled = true
		wait()
		tool.Flash.lite.Enabled = false
		tool.Flash.Light.Enabled = false
	end)
end

shoot_event.OnServerEvent:Connect(function(player, hum)
	if hum ~= "nil" then
		shootEffect()
		if hum.Health <= 30 and hum.Health ~= 0 then
			hum:TakeDamage(30)
			player_killed_event:FireClient(player, hum.Parent.Name)
		elseif hum.Health > 30 then
			hum:TakeDamage(30)
			hit_player:FireClient(player)
		end
	else
		shootEffect()
	end
end)

Thanks for reading!

Using WaitForChild might not be ideal.
You should put the ShootPart variable outside of the script if possible.

Other nitpick is the spawn(). Since it’s being deprecated you can use task.spawn(). Basically putting task. in front of your existing spawns.

1 Like

I wouldn’t make the raycast in client but the server. In that case you can fire a Remote Function which can return the raycast back to the client.

2 Likes

I am raycasting on the client as on the server creates a slight delay.

2 Likes

Thanks for the reply, I always see people using task, what is the difference from using task.wait() and wait()?

1 Like

I would recommend you to use guard clauses for nested if statements.

local arg = "true"
if type(arg) == "boolean" then
     return
end
if #arg >= 5 then
   return
end
print(arg,"passed successfully)
2 Likes

The new task library is more efficient.
The wait is kind of only important if timing needs to be consistent (for example in loops).
The old spawn() apparently has some built in wait which might delay the execution.
It’s also good practice to switch from using deprecated/soon to be deprecated methods because they can stop working at any time.

2 Likes

Thanks for the reply, do you also know a way on how I could make the raycasting more accurate, and prevent it from shooting in the opposite direction of the mouse whenever the camera is in a weird position?

I suggest using camera:screenpointtoray so that it will work when the camera is in a weird angle but you might want to cast a ray where the screen point returned the position and a origin like the character’s position.

2 Likes

Not really a solution but you should check this mouse library made by @EmeraldSlash
EmeraldSlash/RbxMouse: A mouse library providing a consistent and simple interface to Roblox APIs. (github.com)

Thanks for the plug :stuck_out_tongue:

Briefly, the main difference is that my solution raycasts directly from the camera in direction determined by the 2D mouse position.

It does not use a 3D position in the world like mouse.Hit.Position to determine the raycast direction.

Hopefully the usage code for RbxMouse makes it easy to see the difference.

5 Likes

Well I have some questions based off this topic does this use the camera functions screen point to ray or view port to ray to change from 2d to 3d? Also does this use the user input service get mouse location?, because that one is usually inaccurate on where the mouse cursor is.

1 Like

Yep, ScreenPointToRay and ViewportPointToRay convert a 2D position on the screen to a 3D position + direction in the world based on the position and direction of the camera and the 2D position.

UserInputService:GetMouseLocation() is accurate, however it does not include the GUI inset so anything that uses the GUI insets will be slightly inaccurate. To use it in situations with a GUI inset, you first need to subtract the GUI inset from the mouse location value.

ScreenGuis use GUI inset by default, so the mouse location needs to be converted first or the ScreenGui’s IgnoreGuiInset property enabled. (assuming that’s one of the places where you have trouble with mouse location accuracy.)

In Roblox APIs, ‘Viewport’ usually means there is not GUI inset, while ‘Screen’ means there is GUI inset.

e.g. on desktop:

  • Top left corner of your monitor in ‘Viewport’ = (0, 0)
  • Top left corner of your monitor in 'Screen" = (0, -36)

That’s the difference between ViewportPointToRay and ScreenPointToRay.

To find the exact gui inset you can call GuiService:GetGuiInset()

1 Like