How to make a raycasting gun

Preface

Alright so the developer hub and old wiki had this, although a few months ago it was removed from the former because it was just so terribly outdated it was a joke.

So I thought I should revive that old tutorial.

A lot has happened since that tutorial, like new raycasting api being added and filtering enabled being enforced and that tutorial was just never updated.

So I am making this with that all in mind.


Let’s begin!

Alright so first you will want your weapon.

I grabbed a Colt M1911A1 from the toolbox. I have welded each part to the Handle and the welds are in a Folder. The rest of the parts except the Handle and Shoot part are in another Folder called Parts.

The Shoot part will be where the bullets are shot from.

I then added a Script and LocalScript, the former named Backend and latter named Frontend. I then added a RemoteEvent called OnShoot which will be fired when the tool is activated.

Then I put the Tool in StarterPack just so the tool is instantly in your backpack for testing.

Coding the frontend

So I will start with the LocalScript.

First I will declare some variables.

local tool = script.Parent
local remote = tool:WaitForChild("OnShoot")

local Players = game:GetService("Players")
local client = Players.LocalPlayer
local cursor = client:GetMouse() 
Small note

While I don’t like advertising my own stuff I have made a custom mouse module. The vanilla mouse is legacy, and is likely to be deprecated in the future. So I have made this (which exposes PlayerMouse2.Position) as a modern alternative. It isn’t a full replacement though unfortunately due to some restrictions. If you would like to learn how to get the hit position, have a look at this reply of mine, position is either result and result.Position or origin + direction.

But for simplicity’s sake I will just use the legacy mouse.

Pretty straightforward. Now we will want to listen for the tool’s Activated event and just fire the remote with the mouse’s position:

local tool = script.Parent
local remote = tool:WaitForChild("OnShoot")

local Players = game:GetService("Players")
local client = Players.LocalPlayer
local cursor = client:GetMouse() 

tool.Activated:Connect(function()
	remote:FireServer(cursor.Hit.Position)
end)

Then in the backend script you will want to listen for the OnServerEvent event of the OnShoot remote.

Coding the backend

So I am going to steal some variables from the frontend script and connect to OnServerEvent. I will also declare a variable for Workspace, in addition to one for the Shoot part from the tool. no im not using the built-in one but you can if you want

local tool = script.Parent
local shoot_part = tool:WaitForChild("Shoot")
local remote = tool:WaitForChild("OnShoot")

local Workspace = game:GetService("Workspace")

remote.OnServerEvent:Connect(function(player, position)
	-- Nothing
end)

Now to cast a ray.

local tool = script.Parent
local shoot_part = tool:WaitForChild("Shoot")
local remote = tool:WaitForChild("OnShoot")

local Workspace = game:GetService("Workspace")
local ServerStorage = game:GetService("ServerStorage")

remote.OnServerEvent:Connect(function(player, position)
	local origin = shoot_part.Position
	local direction = (position - origin).Unit
	local result = Workspace:Raycast(shoot_part.Position, direction*300)
end)

So here we are just getting the direction of the shoot part position and the mouse position, and extending it for 300 studs since that is what the old tutorial did, although of course you can extend it further or even decrease it.

WorldRoot:Raycast returns nil if the ray cast didn’t hit anything.

local tool = script.Parent
local shoot_part = tool:WaitForChild("Shoot")
local remote = tool:WaitForChild("OnShoot")

local Workspace = game:GetService("Workspace")

remote.OnServerEvent:Connect(function(player, position)
	local origin = shoot_part.Position
	local direction = (position - origin).Unit
	local result = Workspace:Raycast(shoot_part.Position, direction*300)
	
	local intersection = result and result.Position or origin + direction*300
end)

So that is what the result and result.Position or origin + direction*300 is for, since I am pretty sure even if the player doesn’t click on anything you would want a bullet to be shot nonetheless. Logically, if the ray doesn’t hit anything, the “hit position” is origin + direction.

Now to create the bullet. You can pre-make yours, which I already have.

It is just a neon part whose color is New Yeller, sized 0.1, 0.1, 1, it is anchored and has collisions disabled. Although in code the part will be resized. I then put it in ServerStorage.

Continuing.

local tool = script.Parent
local shoot_part = tool:WaitForChild("Shoot")
local remote = tool:WaitForChild("OnShoot")

local Workspace = game:GetService("Workspace")
local ServerStorage = game:GetService("ServerStorage")

remote.OnServerEvent:Connect(function(player, position)
	local origin = shoot_part.Position
	local direction = (position - origin).Unit*300
	local result = Workspace:Raycast(origin, direction)
	
	local intersection = result and result.Position or origin + direction
	local distance = (origin - intersection).Magnitude
	
	local bullet_clone = ServerStorage.Bullet:Clone()
	bullet_clone.Size = Vector3.new(0.1, 0.1, distance)
	bullet_clone.CFrame = CFrame.new(origin, intersection)*CFrame.new(0, 0, -distance/2)
	bullet_clone.Parent = Workspace
end)

So I am doing quite a bit.

First we are getting the distance in studs between the origin and the intersection. Then cloning the bullet and resizing it on the Z axis. Then I am setting its CFrame to the origin of the part, looking towards the intersection. Then parenting to workspace.

Now I want to quickly explain that offset of -distance/2 studs. Without if, the center of the part would be at the origin.

So we want to get one “half” of the part and move it forward.

Now to make the gun deal damage.

local tool = script.Parent
local shoot_part = tool:WaitForChild("Shoot")
local remote = tool:WaitForChild("OnShoot")

local Workspace = game:GetService("Workspace")
local ServerStorage = game:GetService("ServerStorage")

remote.OnServerEvent:Connect(function(player, position)
	local origin = shoot_part.Position
	local direction = (position - origin).Unit*300
	local result = Workspace:Raycast(origin, direction)
	
	local intersection = result and result.Position or origin + direction
	local distance = (origin - intersection).Magnitude
	
	local bullet_clone = ServerStorage.Bullet:Clone()
	bullet_clone.Size = Vector3.new(0.1, 0.1, distance)
	bullet_clone.CFrame = CFrame.new(origin, intersection)*CFrame.new(0, 0, -distance/2)
	bullet_clone.Parent = Workspace
	
	if result then
		-- Nothing
	end
end)

Like I mentioned before, WorldRoot:Raycast returns nil if the ray casted never hit anything. So we want to confirm that it did and from there check if there is a humanoid to damage. Then wait 1/4th of a second (or however long you want really) to delete the bullet!

local tool = script.Parent
local shoot_part = tool:WaitForChild("Shoot")
local remote = tool:WaitForChild("OnShoot")

local Workspace = game:GetService("Workspace")
local ServerStorage = game:GetService("ServerStorage")

remote.OnServerEvent:Connect(function(player, position)
	local origin = shoot_part.Position
	local direction = (position - origin).Unit*300
	local result = Workspace:Raycast(origin, direction)
	
	local intersection = result and result.Position or origin + direction
	local distance = (origin - intersection).Magnitude
	
	local bullet_clone = ServerStorage.Bullet:Clone()
	bullet_clone.Size = Vector3.new(0.1, 0.1, distance)
	bullet_clone.CFrame = CFrame.new(origin, intersection)*CFrame.new(0, 0, -distance/2)
	bullet_clone.Parent = Workspace
	
	if result then
		local part = result.Instance
		local humanoid = part.Parent:FindFirstChild("Humanoid") or part.Parent.Parent:FindFirstChild("Humanoid")
		
		if humanoid then
			humanoid:TakeDamage(10)
		end
	end
	
	wait(0.25)
	bullet_clone:Destroy()
end)

Video in action!

296 Likes

Thank you so much for making this tutorial! I’m sure many developers will want to know how to do this. If I do decide to use this in the future, I’ll be sure to credit you! :happy3:

28 Likes

Thanks for the tutorial Incapaz, great work as always.

However, is there any actual way to implement sanity checks in RemoteEvent? It’s pretty easy to spam it from what I can tell.

9 Likes

There is, although i didn’t want to make a full-on weapons tutorial accounting for every single little detail since there are various on that already, this is just so you can start getting the gun to actually shoot.

The production code should definitely have some sort of rate-limiting mechanism though

12 Likes

Example sanity checks that can be implemented from the code:

  • Rate-limit
  • Range-limit (preventing the barrel from being too far from the player as the character has full control of the gun as a tool, thus exploitation may be possible)

This tutorial does one thing better than the original which is explaining this particular line of code:
CFrame.new(origin, intersection)*CFrame.new(0, 0, -distance/2) which normally beginners might have trouble with.

Also, for anyone who follows the tutorial, doing this part.Parent:FindFirstChild("Humanoid") or part.Parent.Parent:FindFirstChild("Humanoid") when detecting a part via ray may be more dangerous than part.Parent:FindFirstChild("Humanoid") only, because accessories are separate hitboxes so it may be undesirable if a player can be damaged just by shooting a huge hat or large accessory.

19 Likes

Man, I wish I was able to see this tutorial when I started scripting for the first time. I do hope other people will be able to see this tutorial and make use of it. In other words, thanks for the tutorial!

10 Likes

This tutorial is made really nice! I like this tutorial a lot. Good job!

10 Likes

A good way to learn how to use raycasting, thanks.

8 Likes

Thank you for the tutorial, this will help new developers to learn how to make raycasting guns, also why couldn’t you just use the built-in workspace function that has a reference to the Workspace instead of doing local Workspace = game:GetService("Workspace")? it’s easier to just use the first one.

I also have a suggestion, could you possibly make a simple tutorial that would teach us how to create a simple gun with bullets instead of raycasting this time?

If you want an example of this then take a look at the Roblox game Arsenal which have guns with bullets, I’m sure that there are people who would like to learn how to make that too! :slightly_smiling_face:

8 Likes

Thanks for the tutorial! This will really help with my Lua knowledge.

7 Likes

Oh hey I have a question about that, would this:

local direction = (position - origin).Unit*300

have a different result of this?

local direction = (origin - position).Unit*300

Because I dont really understand why you need to do (endVector - startVector) instead of (startVector - endVector)

4 Likes

Yes there is a difference.

v = (a - b).Unit gives the vector from b to a.
Likewise,
u = (b - a).Unit would give the vector from a to b .

u would equal -v.

6 Likes

Wait so it means that its actually meant to be like that?

I thought I had to do

(startPos - goalPos).Unit

Because it seems more logical

3 Likes

Here, let me show you an example.

The front face of the origin has the attachment.

The front face of this target also has an attachment at the front.

Once more, this visual part has an attachment as well.

If you run this code subtracting the ending position from the origin instead of vice-versa, the result is not what you want.

First, doing how I did it.

local Workspace = game:GetService("Workspace")

local origin = Workspace:WaitForChild("origin"):WaitForChild("Attachment").WorldPosition
local target = Workspace:WaitForChild("target"):WaitForChild("Attachment").WorldPosition
local visual = Workspace:WaitForChild("visual")

local result = Workspace:Raycast(origin, (target - origin).Unit*25)

local intersection = result and result.Position or origin + (target - origin).Unit*25

if result then
	print(result.Instance)
	local distance = (origin - intersection).Magnitude
	visual.Size = Vector3.new(visual.Size.X, visual.Size.Y, distance)
	visual.CFrame = CFrame.new(origin, result.Position)*CFrame.new(0, 0, -distance/2)
end

I get target in the output, cool.

Now doing it your way.

I don’t even know how to explain it but that is what happened.

11 Likes

Oh well thanks for your help though

2 Likes

Will this work for mobile devices?

3 Likes

Since it uses the peripheral target of the mouse, the object it is hovering over or put on, no. This only works for devices like a PC.

3 Likes

Could you be kind enough to guide me how to make it mobile friendly . . The cursor / tap to shot system from weapons kit game is what I hope to achieve . .

4 Likes

Construct a raycastparams, then set its FilterType to Enum.RaycastFilterType.Blacklist and add the character to the FilterDescendantsInstances.

local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Blacklist
params.FilterDescendantsInstances = { player.Character }

...
local result = Workspace:Raycast(origin, direction, params)
9 Likes

My friend hired me to script a gun and I’ll be definitely using this. However, this is only single shot, is there a way to make it automatic?

3 Likes