FPS Tools Using ViewModels Tutorial (With Working ADS!)[R6/R15 Compatible]

Hello! I’ve been experimenting with FPS mechanics recently and I was able to make working FPS guns using ViewModels, so I will write a tutorial on how I made it!

You need some coding experience to understand this tutorial. This is my first community tutorial, so correct me if I made any mistakes!
Thanks to @Ancientroboman for helping me while making the ViewModel!

Plugins/Models used in this tutorial

Notes

While making your gun, make sure the avatar type you will be using is the same as the avatar type in game settings, guns in this tutorial support both of R6 and R15.

What Should be expected


R15:

Step 1: Creating the Tool

Step 2: Coding the ViewModel & Creating Basic Animation

Scripting

Now we can start scripting, insert a LocalScript and a ModuleScript into the gun, the ModuleScript will be used by the client only for animations and effects (We will get to this part later in the tutorial)
image
And we finally start writing the script, in the LocalAnimations module script, we add this

local animations = {}

animations.EquipTime = 15 / 60 -- // 15 frames out of 60
animations.HoldAnimation = 6283727388 -- // REPLACE THIS WITH YOUR OWN ANIMATION ID!!!

return animations

EquipTime is the time taken to equip the tool, to make our Equip/Unequip animations we will be adjusting the speed of the animation, when the animation reachs the equip time it will have speed adjusted to 0, when we are unequipping it will adjust the speed to -1.
HoldAnimation is the animation ID that we saved from earlier.

Now we script the main gun script!

local Cam = workspace.CurrentCamera
local Player = game.Players.LocalPlayer
local Char = Player.Character
while Char == nil or Char.Parent == nil do
	Char = Player.Character
	wait()
end

local Humanoid = Char:WaitForChild("Humanoid")
local Animator = Humanoid:WaitForChild("Animator", 1) or Humanoid -- // In-case Animator didn't load, use humanoid instead to prevent errors

local AnimationConfig = require(script.Parent:WaitForChild("LocalAnimations")) -- // Loading out animation config


-- // This function will load animations in a table if it's requested to play for the first time // --
local LoadedAnimations = {}
local function animation(ID, stop, weight, speed, reset)
	local Anim = LoadedAnimations[tostring(ID)]
	if not Anim then -- // If animation isn't loaded, load it // --
		local AnimInstance = Instance.new("Animation", script)
		AnimInstance.AnimationId = "rbxassetid://" .. tostring(ID)
		
		Anim = {
			Inst = AnimInstance,
			Global = Animator:LoadAnimation(AnimInstance),
			Local = nil -- // This will be the animation that loads on the local ViewModel, we will get to this part later in the tutorial. // --
		}
		LoadedAnimations[tostring(ID)] = Anim
	end
	
	if stop then
		Anim.Global:Stop()
	else
		if (Anim.Global.IsPlaying == false) or reset then -- // Play the animation if it isn't playing already, or if reset is set to true // --
			Anim.Global:Play()
		end
	end
	
	if weight then
		Anim.Global:AdjustWeight(weight)
	end
	if speed then
		Anim.Global:AdjustSpeed(speed)
	end
	return Anim.Local
end


local idleAnim = animation(AnimationConfig.HoldAnimation, true) -- // Preload animation


local TickEquipped = 0
local Equipped = false
function checkEquipped()
	if script.Parent.Parent == Char and Equipped == false then
		Equipped = true
		TickEquipped = tick()
		
		animation(AnimationConfig.HoldAnimation)
	elseif Equipped == true then
		Equipped = false
		TickEquipped = tick()
		
		animation(AnimationConfig.HoldAnimation, false, nil, -1)
	end
end
spawn(checkEquipped)
script.Parent:GetPropertyChangedSignal("Parent"):Connect(checkEquipped)

game:GetService("RunService").RenderStepped:Connect(function(deltaTime)
	if Equipped then
		if idleAnim.TimePosition >= AnimationConfig.EquipTime then
			animation(AnimationConfig.HoldAnimation, false, nil, 0)
		end
	end
end)

This script will preload the animation and play it, most parts of the script have been commented.
Now we need to make a new script in StarterCharacterScripts for the ViewModel.
image
This is how it would work:

  1. Copy the player’s character while deleting it’s legs and putting it inside of the player’s camera.
  2. Setting the CollisionGroup of all parts of the ViewModel to a collision group that cannot collide with any other parts.
  3. Each time a tool is equipped, it will auto-weld it self to the ViewModel’s arms (From the tool’s GunScript)

We can use Collision group editor plugin to create and configure our collision group
image
ViewModel Script:

local PhysicsService = game:GetService("PhysicsService")

local Cam = workspace.CurrentCamera
if Cam:FindFirstChild("ViewModel") then
	Cam.ViewModel:Destroy() -- // Destroy old ViewModel if present // --
end
local Player = game.Players.LocalPlayer
local Char = script.Parent

Char.Archivable = true
local ViewModel = Char:Clone()
Char.Archivable = false

ViewModel.Parent = Cam
ViewModel.Name = "ViewModel"
ViewModel.PrimaryPart = ViewModel.Head
ViewModel.PrimaryPart.Anchored = true
ViewModel:SetPrimaryPartCFrame(CFrame.new(0, 5, 0))

-- // Setting ViewModel collisions, deleting unnecessary leg parts and preventing it from casting shadows. // --
for i,v in pairs(ViewModel:GetChildren()) do
	if v:IsA("BasePart") then
		v.CastShadow = false
		PhysicsService:SetPartCollisionGroup(v, "NoCollisionViewModel")
		if v.Name:lower():match("leg") or v.Name:lower():match("foot") then
			v:Destroy()
		end
	elseif v:IsA("BaseScript") then
		v:Destroy()
	end
end

for i,v in pairs(ViewModel.Head:GetChildren()) do
	if v:IsA("Decal") then v:Destroy() end -- // Delete all faces // --
end

game:GetService("RunService").RenderStepped:Connect(function()
	local Tool = Char:FindFirstChildWhichIsA("Tool")
	if Tool and Tool:FindFirstChild("GunScript") then -- // If there is a tool, and it's a gun then make the ViewModel arms visible // --
		
		for i,v in pairs(ViewModel:GetChildren()) do
			if v:IsA("BasePart") then
				if v.name:lower():match("arm") or v.name:lower():match("hand") then
					v.Transparency = 0
					v.LocalTransparencyModifier = 0
				else
					v.Transparency = 1
					v.LocalTransparencyModifier = 1
				end
			end
		end
		
		-- // Make the character arms invisible // --
		for i,v in pairs(Char:GetChildren()) do
			if v:IsA("BasePart") then
				v.Transparency = 1
				v.LocalTransparencyModifier = 1
			end
		end
	else
		for i,v in pairs(ViewModel:GetChildren()) do
			if v:IsA("BasePart") then
				v.Transparency = 1
				v.LocalTransparencyModifier = 1
			end
		end
		
		for i,v in pairs(Char:GetChildren()) do
			if v:IsA("BasePart") then
				if v.name:lower():match("arm") or v.name:lower():match("hand") then
					v.Transparency = 0
					v.LocalTransparencyModifier = 0
				else
					v.Transparency = 1
					v.LocalTransparencyModifier = 1
				end
			end
		end
	end
	
	-- // Copy cloths and body colors from main character to ViewModel // --
	
	local Clothing = Char:FindFirstChildWhichIsA("Shirt")
	if Clothing then
		local Shirt = ViewModel:FindFirstChildWhichIsA("Shirt") or Instance.new("Shirt", ViewModel)
		Shirt.ShirtTemplate = Clothing.ShirtTemplate
	else
		local Shirt = ViewModel:FindFirstChildWhichIsA("Shirt") or Instance.new("Shirt", ViewModel)
		Shirt.ShirtTemplate = ""
	end
	
	for i,v in pairs(ViewModel:GetChildren()) do
		if v:IsA("BasePart") then
			v.Color = Char[v.Name].Color
		end
	end
end)


Now we have a working ViewModel, we just need to weld the gun to it and set it’s position.
We will have to weld the gun from the server, so we need to add a Script in ServerScriptService.

local function weldTool(Tool, CharHand)
	if Tool:IsA("Tool") and Tool:FindFirstChild("GunScript") and Tool:FindFirstChild("GripPart") and not Tool.GripPart:FindFirstChild("ArmWeld") then
		local Grip = Tool.Grip
		if CharHand.Name == "Right Arm" then -- // Since R15 and R6 grips are different, we will have to apply some rotation to the grip // --
			Grip = Grip * CFrame.Angles(math.pi/2,0,0)
		end
		
		local GripMotor = Instance.new("Motor6D", Tool.GripPart)
		GripMotor.C0 = CharHand:WaitForChild("RightGripAttachment").CFrame--:ToWorldSpace(Tool.Grip:Inverse()) -- // Get the original tool grip, and apply it to the Motor6D // --
		GripMotor.C1 = Grip
		GripMotor.Part0 = CharHand
		GripMotor.Part1 = Tool.GripPart
		GripMotor.Enabled = true
		GripMotor.Name = "ArmWeld"
	end
end

game.Players.PlayerAdded:Connect(function(Player)
	Player.CharacterAdded:Connect(function(Char)
		local CharHand
		if Char:WaitForChild("Humanoid", 5).RigType == Enum.HumanoidRigType.R15 then
			CharHand = Char:WaitForChild("RightHand", 5)
		else
			CharHand = Char:WaitForChild("Right Arm", 5)
		end
		
		Char.ChildAdded:Connect(function(Child)
			weldTool(Child, CharHand)
		end)
		for i,v in pairs(Char:GetChildren()) do
			weldTool(v, CharHand)
		end
	end)
end)

For the script to work, we will have to rename Handle to GripPart, you should keep the name as Handle for the animation clone so that studio automatically welds it for you while animating.

Back to our GunScript, we will include the variables required for the ViewModel and weld the gun to it instead of the main character’s arms.

local Grip = script.Parent:WaitForChild("GripPart")

local ViewModel = Cam:WaitForChild("ViewModel")
local ViewModelAnimator = ViewModel:WaitForChild("Humanoid")
ViewModelAnimator = ViewModelAnimator:WaitForChild("Animator", 1) or ViewModelAnimator
if Humanoid.RigType == Enum.HumanoidRigType.R15 then
	ViewModelArm = ViewModel:WaitForChild("RightHand")
else
	ViewModelArm = ViewModel:WaitForChild("Right Arm")
end

Grip:WaitForChild("ArmWeld", math.huge).Part0 = ViewModelArm

Now we set the ViewModel’s position, we change the RunService event to this

game:GetService("RunService").RenderStepped:Connect(function(deltaTime)
	if Equipped then
		if tick() - TickEquipped >= AnimationConfig.EquipTime then
			animation(AnimationConfig.HoldAnimation, false, nil, 0)
		end
		
		ViewModel:SetPrimaryPartCFrame(Cam.CFrame)
	end
end)

Then we play animations on both the ViewModel and main character, by changing our animation function to this:

-- // This function will load animations in a table if it's requested to play for the first time // --
local LoadedAnimations = {}
local function animation(ID, stop, weight, speed, reset)
	local Anim = LoadedAnimations[tostring(ID)]
	if not Anim then -- // If animation isn't loaded, load it // --
		local AnimInstance = Instance.new("Animation", script)
		AnimInstance.AnimationId = "rbxassetid://" .. tostring(ID)
		
		Anim = {
			Inst = AnimInstance,
			Global = Animator:LoadAnimation(AnimInstance),
			Local = ViewModelAnimator:LoadAnimation(AnimInstance) -- // This will be the animation that loads on the local ViewModel, we will get to this part later in the tutorial. // --
		}
		LoadedAnimations[tostring(ID)] = Anim
	end
	
	if stop then
		Anim.Global:Stop()
		Anim.Local:Stop()
	else
		if (Anim.Global.IsPlaying == false) or reset then -- // Play the animation if it isn't playing already, or if reset is set to true // --
			Anim.Global:Play()
			Anim.Local:Play()
		end
	end
	
	if weight then
		Anim.Global:AdjustWeight(weight)
		Anim.Local:AdjustWeight(weight)
	end
	if speed then
		Anim.Global:AdjustSpeed(speed)
		Anim.Local:AdjustWeight(weight)
	end
	return Anim.Local
end

We should have this result:

Step 3: Scripting the ADS

Since we now have the gun function with animations and all, we can finally start with the ADS.

How it will work is:

  1. An AimPart is put into the gun, which will have a grip Vector3 set in LocalAnimations module.
  2. A regular grip Vector3 set in LocalAnimations where AimPart is placed into when the player is not aiming.
  3. Smooth tweening between both grips during aiming.
  4. While playing animations like reloading animations, firing animations, etc that will require the AimPart to go out of grip position, we will set the ViewModel’s Head CFrame instead (PrimaryPart), how this will work is that whenever the gun is not animating (Idle Animation), it will save the offset of ViewModel’s Head CFrame relative to camera in a variable, and whenever it’s animating it will use that offset instead of setting AimPart position, we will be constantly updating that offset to avoid wrong offsets saving and breaking the gun.

Our ADS will only display on client side, meaning other players cannot see if another player is aiming or not.

  • First, we create an invisible AimPart in our gun and weld it (Already inside the model we inserted earlier)

  • Now we set the grips in our ModuleScript

local animations = {}

animations.EquipTime = 10 / 60 -- // 10 frames out of 60
animations.HoldAnimation = 6283727388 -- // REPLACE THIS WITH YOUR OWN ANIMATION ID!!!

animations.holdGrip = Vector3.new(0.5, -0.5, -0.5)
animations.aimGrip = Vector3.new(0, 0, 0.3)

return animations
  • To check for aiming, we can use Player’s Mouse to check when he holds right mouse button, we add these variables at the start of our script
local TweenService = game:GetService("TweenService")
local UserInputService = game:GetService("UserInputService") -- // We will use this to make the mouse invisible while holding the gun
local Mouse = Player:GetMouse()
local AimPart = script.Parent:WaitForChild("AimPart")
local Aiming = false
local CurrentGrip = Instance.new("NumberValue", script) -- // Will be used to tween between grip positions

And we change our checkEquipped() function to this so it stops aiming when you are no longer holding the gun.

function checkEquipped()
	if script.Parent.Parent == Char and Equipped == false then
		Equipped = true
		Aiming = false
		CurrentGrip.Value = 0
		TickEquipped = tick()
		
		animation(AnimationConfig.HoldAnimation)
	elseif Equipped == true then
		Equipped = false
		Aiming = false
		CurrentGrip.Value = 0
		TickEquipped = tick()
		
		animation(AnimationConfig.HoldAnimation, false, nil, -1)
	end
end

Then we need to listen when the player starts holding right mouse button and when he stops holding it:

function toggleAim(State)
	if State == Aiming then return end -- // Don't aim if you are already aiming // --
	Aiming = State
	if Aiming == true then
		TweenService:Create(CurrentGrip, TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut), {Value = 1}):Play()
	else
		TweenService:Create(CurrentGrip, TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut), {Value = 0}):Play()
	end
end

Mouse.Button2Down:Connect(function()
	toggleAim(Equipped) -- // If the gun is equipped Aiming will be set to true, and if it's not equipped Aiming will be set to false
end)

Mouse.Button2Up:Connect(function()
	toggleAim(false)
end)

And we change the RunService event to this:

game:GetService("RunService").RenderStepped:Connect(function(deltaTime)
	if Equipped then
		UserInputService.MouseIconEnabled = false
		if tick() - TickEquipped >= AnimationConfig.EquipTime then
			animation(AnimationConfig.HoldAnimation, false, nil, 0)
		end
		
		AimPart.CFrame = Cam.CFrame:ToWorldSpace(CFrame.new(AnimationConfig.holdGrip, Vector3.new(0, 0, -100)):Lerp(CFrame.new(AnimationConfig.aimGrip, Vector3.new(0, 0, -100)), CurrentGrip.Value)) -- // Set the CFrame of the AimPart to the current grip relative to the camera, while looking front // --
	else
		UserInputService.MouseIconEnabled = true
	end
end)

Now the gun can do ADS perfectly fine but we still need to make it able to play animations and be able to move AimPart, so we will add this variable right before the RenderStepped event:

local HeadHoldOffset -- // The Offset at which the head is positioned relative to camera, this is used instead of setting AimPart CFrame while animating, it will be nil until set in the RenderStepped loop

And change the RenderStepped event to this:

game:GetService("RunService").RenderStepped:Connect(function(deltaTime)
	if Equipped then
		if not HeadHoldOffset then -- // Else if the HeadHoldOffset is not set at all, it will play the animation and set it
			idleAnim:Play()
			idleAnim.TimePosition = AnimationConfig.TimePosition
			wait(.2)
			AimPart.CFrame = Cam.CFrame:ToWorldSpace(CFrame.new(AnimationConfig.holdGrip, Vector3.new(0, 0, -100))) -- // Set the CFrame of the AimPart to the current grip relative to the camera, while looking front // --
			HeadHoldOffset = Cam.CFrame:ToObjectSpace(ViewModel.PrimaryPart.CFrame)
		elseif CurrentGrip.Value == 0 and idleAnim.IsPlaying and idleAnim.TimePosition >= AnimationConfig.EquipTime then -- // If the AimPart is placed in correct grip position & the idle animation is playing // --
			AimPart.CFrame = Cam.CFrame:ToWorldSpace(CFrame.new(AnimationConfig.holdGrip, Vector3.new(0, 0, -100)))
			HeadHoldOffset = Cam.CFrame:ToObjectSpace(ViewModel.PrimaryPart.CFrame)
		end
		
		UserInputService.MouseIconEnabled = false
		if idleAnim.TimePosition >= AnimationConfig.EquipTime then
			animation(AnimationConfig.HoldAnimation, false, nil, 0)
		end
		
		if HeadHoldOffset then
			ViewModel:SetPrimaryPartCFrame(Cam.CFrame:ToWorldSpace(HeadHoldOffset))
			AimPart.CFrame = AimPart.CFrame:Lerp(Cam.CFrame:ToWorldSpace(CFrame.new(AnimationConfig.aimGrip, Vector3.new(0, 0, -100))), CurrentGrip.Value) -- // Lerp the AimPart CFrame to aimGrip if the player is aiming. // --
		end
	else
		UserInputService.MouseIconEnabled = true
	end
end)

And we are done!


If you have any problems or found mistakes in my post feel free to reply to this topic and I’ll answer you once I am able to!

Thanks for reading and good luck with your coding : )

Place file: Tutorial.rbxl (316.8 KB)

If you want a new part about making reloading and firing, you can vote on this poll:
Instead of making a new part, I will be completely rewriting the tutorial to be cleaner and make the ViewModel better soon.

Make a new part
  • Yes
  • No

0 voters

15 Likes

Here is the place if you want to try it out https://www.roblox.com/games/6283372435/Tutorial
I’ll have to go right now, so I might reply to your comments tomorrow!

2 Likes

I’ve updated the post to fix some mistakes and the AimPart animation problem + added R15 result video, to use R15 all you need to do is change avatar type to R15 instead of R6 and make sure your animations are R15 animations

So I wanted to try this out because it had a LOT less scripts than my other FPS projects although,

This happens whenever I start the game. Any assistance?

Could you post your output and scripts in DMs?

(I will rewrite this tutorial soon)

Great tutorial! Always had trouble with making FPS mechanics, especially the viewmodel aspect. I found your tutorial to be concise and quick to setup, super helpful!

When can we expect reloading and firing?? Looking forward to it!

1 Like

I think for reloading and firing I will rewrite the tutorial to make the viewmodel much smoother and easier to explain, plus it will support making all tools in your game FPS instead of just guns

1 Like