FPS using ViewModels (The improved version) // Parts: 2 out of 3

IMPORTANT NOTE

I’ve been generally inactive on roblox and in development recently. I’ve stopped supporting this tutorial further, and any questions will probably not be answered. I recommend seeing a better tutorial such as this one by EgoMoose.

Introduction

I’ve previously made this tutorial about making FPS guns using ViewModels but the tutorial was not well made since it was my first one. I’ve decided to rewrite the whole tutorial in multiple parts this time (On this same topic) with this first part being about creating the ViewModel and playing animations on it only.

This ViewModel works for both R6 and R15 and it doesn’t require you to manually play animations on each of the player’s character and the ViewModel, instead it will copy animations from the player’s character and play them on the ViewModel if these animations had an attribute called ViewModelAnimation set to true (To prevent playing emote animations and walk animations in first person).

Please note that the animations will not work if they are loaded onto Humanoid instead of Humanoid.Animator, Please use Animator for all your animations!

I’m sorry for the huge delay of this tutorial, I’ll try my best to complete the full tutorial this time!
Also please note that the tutorial is not fully finished yet, it will be consisting of 3 parts, ViewModel, Aim down sights and finally reload animations.

Expected Final Result

Part 1: The ViewModel

The ViewModel is the fake pair of arms that appear on your screen in FPS games, the way we will be creating our ViewModel is by cloning the player’s character and deleting unnecessary parts (Legs, Face and Accessories).
Let’s start by creating a script called ViewModel in StarterCharacterScripts.

image

Now we have to load some Services we will be using, RunService and PhysicsService.

local RunService = game:GetService("RunService")
local PhysicsService = game:GetService("PhysicsService")

I’ll explain later why we would need them.

Since the ViewModel should always be placed where the camera is, we will need the player’s camera too, We will also be using it as a parent for the ViewModel.

local Camera = workspace.CurrentCamera

Everytime a player character is loaded, this script is inserted into the character, so it will keep creating new ViewModels everytime and we have to delete old ViewModels.

local Camera = workspace.CurrentCamera
if Camera:FindFirstChild("ViewModel") then
	Camera.ViewModel:Destroy()
end

Let’s create a variable for the character and another for it’s Animator.

local Character = script.Parent
local Animator = Character:WaitForChild("Humanoid"):WaitForChild("Animator") -- // We need the animator to replicate animations to the ViewModel.

Now the part where we actually create the ViewModel, we clone the player’s character.

local ViewModel = Character:Clone()
ViewModel.Parent = Camera
ViewModel.Name = "ViewModel"

The code above will not work since the Character’s Archivable property is set to false, meaning we cannot clone it, so we have to set it to true first.

Character.Archivable = true
local ViewModel = Character:Clone()
ViewModel.Parent = Camera
ViewModel.Name = "ViewModel"
Character.Archivable = false

Let’s do some configuration…

local ViewModelAnimator = ViewModel.Humanoid.Animator -- // Used to play animations on the ViewModel.
ViewModel.Humanoid.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None -- // Disable name display.
ViewModel.Humanoid.HealthDisplayType = Enum.HumanoidHealthDisplayType.AlwaysOff -- // Disable health display.
ViewModel.Humanoid.BreakJointsOnDeath = false

We will place the ViewModel in air for now until we get to the part where we place it at the camera.

ViewModel.Head.Anchored = true
ViewModel.PrimaryPart = ViewModel.Head
ViewModel:SetPrimaryPartCFrame(CFrame.new(0, 5, 10))

image

Perfect!
We want to delete unnecessary parts, so we will go through all descendants of the ViewModel and change them a little bit.

for _, Part in pairs(ViewModel:GetDescendants()) do
	if Part:IsA("BasePart") then
		Part.CastShadow = false -- // If it's a part, then disable it's shadow.
		
		local LowerName = Part.Name:lower() -- // Get the lowercase part name
		if LowerName:match("leg") or LowerName:match("foot") then
			Part:Destroy() -- // If this is a leg/foot part then delete it, since it's useless.
		elseif not (LowerName:match("arm") or LowerName:match("hand")) then
			Part.Transparency = 1 -- // If this part isn't a part of the arms then it should be invisible.
		end
	elseif Part:IsA("Decal") then
		Part:Destroy() -- // Delete all decals (Face).
	elseif Part:IsA("Accessory") then
		Part:Destroy() -- // Delete all accessories.
	elseif Part:IsA("LocalScript") then
		Part:Destroy() -- // Destroy all scripts.
	end
end

Now it’s working perfectly, except we still collide with the ViewModel, now that’s where we start using PhysicsService.
Through studio’s Model tab, click on Collision Groups to open a window where we can configure our collision groups.

Now create a new collision group called ViewModel and configure it like this

image

Back to our script, in the same loop that deletes unnecessary parts, we add this line

PhysicsService:SetPartCollisionGroup(Part, "ViewModel")

The full loop should look like this now

for _, Part in pairs(ViewModel:GetDescendants()) do
	if Part:IsA("BasePart") then
		Part.CastShadow = false -- // If it's a part, then disable it's shadow.
		PhysicsService:SetPartCollisionGroup(Part, "ViewModel")
		
		local LowerName = Part.Name:lower() -- // Get the lowercase part name
		if LowerName:match("leg") or LowerName:match("foot") then
			Part:Destroy() -- // If this is a leg/foot part then delete it, since it's useless.
		elseif not (LowerName:match("arm") or LowerName:match("hand")) then
			Part.Transparency = 1 -- // If this part isn't a part of the arms then it should be invisible.
		end
	elseif Part:IsA("Decal") then
		Part:Destroy() -- // Delete all decals (Face).
	elseif Part:IsA("Accessory") then
		Part:Destroy() -- // Delete all accessories.
	elseif Part:IsA("LocalScript") then
		Part:Destroy() -- // Destroy all scripts.
	end
end

NOTE: Please make sure all parts of your tools are CanCollide off and Massless on since if they weren’t they can cause problems such as flinging the player.

Animations

Now we need to start playing animations on our ViewModel, how we will be doing it is by checking current animations running on the player’s character, if they have the Attribute ViewModelAnimation set to true then the animation will play on the ViewModel.
Now first we will create a simple tool to play animations on the character.

image

local Player = game.Players.LocalPlayer
local Char = Player.Character or Player.CharacterAdded:Wait()
if not Char.Parent then Char = Player.CharacterAdded:Wait() end -- // Player.Character can be the old character sometimes.
local Animator = Char:WaitForChild("Humanoid"):WaitForChild("Animator")

local Animation = Instance.new("Animation", script.Parent)
Animation.AnimationId = "rbxassetid://YOUR_ANIMATION_ID" -- // Please note that you HAVE TO change this ID to your own animation ID since this animation was made by me and will only work if you are playing in my own place.
Animation = Animator:LoadAnimation(Animation)
Animation:SetAttribute("ViewModelAnimation", true)

script.Parent.Equipped:Connect(function()
	Animation:Play()
end)

script.Parent.Unequipped:Connect(function()
	Animation:Stop()
end)

When we test this out we should have this

We will watch for every AnimationTrack played on the character using Animator.AnimationPlayed.

local LoadedAnimations = {}
Animator.AnimationPlayed:Connect(function(AnimationTrack)
	if AnimationTrack:GetAttribute("ViewModelAnimation") ~= true then return end -- // Skip animation if it isn't supposed to play on ViewModel.
	if not LoadedAnimations[AnimationTrack] then -- // Indexing using the animation track.
		-- // If this animation was not already laoded then load it.
		LoadedAnimations[AnimationTrack] = ViewModelAnimator:LoadAnimation(AnimationTrack.Animation) -- // Load animation on the ViewModel.
	end
end)

We will also be manually playing the animation by setting it’s TimePosition every frame, that’s where we will start using RunService.

local function updateAnimations()
	
end

RunService.RenderStepped:Connect(function(dl)
	updateAnimations()
end)

We have to replicate 3 things from the character animations, if the animation is playing, the time position the animation is at and the Weight of it.

local function updateAnimations()
	for CharAnim, Anim in pairs(LoadedAnimations) do
		if CharAnim.IsPlaying ~= Anim.IsPlaying then
			if CharAnim.IsPlaying then
				Anim:Play()
			else
				Anim:Stop()
			end
		end
		
		Anim.TimePosition = CharAnim.TimePosition
		Anim:AdjustWeight(CharAnim.WeightCurrent, 0) -- // 0 Fade time so it's instantly set.
	end
end

If we playtest we should have this result

If it doesn’t look like this for you, review your code and make sure that you have used your own animation ID in the test tool since my own animations will not work in your place.

Welding the tool to the ViewModel

We can finally weld the tool to the ViewModel now, how we will be doing it is whenever a tool is inserted into the character, roblox creates a weld called RightGrip which connects the player’s arm to the Handle of the tool, we will be changing the Part0 of that weld to be the arm in the ViewModel instead of the arm in the character.
Now since roblox seems to break the tool if we change Part0 right once it’s added to the character, we will have to use a wait() before setting it, OR we will have to code our own tool system, including welding the tools, backpack UI and etc, since that is not our main focus in this tutorial I will only be using the first method.

Character.DescendantAdded:Connect(function(Obj)
	if Obj:IsA("Weld") and Obj.Name == "RightGrip" then
		wait()
		Obj.Part0 = ViewModel[Obj.Part0.Name]
	end
end)

Now the tool welds to the ViewModel, perfect!
Let’s place the ViewModel at the camera now.

RunService.RenderStepped:Connect(function(dl)
	updateAnimations()
	ViewModel:SetPrimaryPartCFrame(Camera.CFrame)
end)

The ViewModel should be placed where it should be now!

But one more thing before finishing the tutorial, your game might be changing the shirt of the character or it’s BodyColors, so we have to replicate that to the ViewModel too.

RunService.RenderStepped:Connect(function(dl)
	updateAnimations()
	ViewModel:SetPrimaryPartCFrame(Camera.CFrame)
	
	local ViewModelShirt = ViewModel:FindFirstChildWhichIsA("Shirt") or Instance.new("Shirt", ViewModel) -- // Create a shirt if there is no shirt found.
	local CharacterShirt = Character:FindFirstChildWhichIsA("Shirt")
	if CharacterShirt then
		-- // If a shirt was found in the player's character, then set the ViewModel's shirt to the same shirt.
		ViewModelShirt.ShirtTemplate = CharacterShirt.ShirtTemplate
	end
	
	for _, Part in pairs(ViewModel:GetChildren()) do
		if Part:IsA("BasePart") then
			-- // Set the color of each part of the ViewModel to the color of the part with same name in the character.
			Part.Color = Character[Part.Name].Color
		end
	end
end)

It’s done! Your ViewModel should be perfectly working now, if you have any questions you can send them in the replies, and if you need the uncopylocked place then here is the link.

Next part will be focused around making working Aim Down Sights (ADS for short) and adding recoil effects!

77 Likes

This is actually really helpful! I was just wondering how to do this and this tutorial seems much better compared to the last one. Thanks! Can’t wait to see the next part! :slight_smile:

1 Like

Great tutorial! I need to ask, will this work if the ViewModel is R6 and the character is R15?

2 Likes

Nice tutorial. This tutorial is really helpful.

1 Like

You could have used table indexing instead of individual if/elseif statements for this or instead just cloning the arms without the rest of the body.

1 Like

The tutorial was mainly directed towards beginners, I know there are more efficient ways to do this, but it will be making the code more complicated. Plus this part only runs once when the script starts so you don’t have to worry about it.

I don’t just clone the arms since I’m playing the character’s animations, meaning I require the torso, and the head (I will use the head movement in the next part). Also this code is supposed to work with both R6 and R15 without the need to change anything in it, you just change the game’s avatar type and your tool animations.

You don’t always need to make your code as small as possible, it will just make your code more complicated and give you some problems later, I have tried doing that before, making stuff like these in tables, now I have to rewrite that whole code and use less tables.

1 Like

If you want that you would have to put a pre-made R6 dummy in, let’s say ReplicatedStorage, and when the script loads you make it clone that dummy instead of the player’s actual character.

This will break the body colors part though, since part names are not the same and you will have to insert their names manually.
You will also not be able to play R15 animations on an R6 rig, you can change the animation part of the code so you have to play R6 animations from your tool code but it will need more animation work.

2 Likes

Part 2: ADS & Recoil (Physically Based)

Hello there! This is part 2 of this tutorial which will be focused on Aim Down Sights and Physically Based Recoil using Springs!

Final result

We will need some stuff from previous tutorial to start on this one, the previous gun script and some changes to the gun model.

Original gun script (Only plays animation)

local Player = game.Players.LocalPlayer
local Char = Player.Character or Player.CharacterAdded:Wait()
if not Char.Parent then Char = Player.CharacterAdded:Wait() end -- // Player.Character can be the old character sometimes.
local Animator = Char:WaitForChild("Humanoid"):WaitForChild("Animator")

local Animation = Instance.new("Animation", script.Parent)
Animation.AnimationId = "rbxassetid://6283727388" -- // Please note that you HAVE TO change this ID to your own animation ID since this animation was made by me and will only work if you are playing in my own place.
Animation = Animator:LoadAnimation(Animation)
Animation:SetAttribute("ViewModelAnimation", true)

script.Parent.Equipped:Connect(function()
	Animation:Play()
end)

script.Parent.Unequipped:Connect(function()
	Animation:Stop()
end)

Now in the gun we need to modify 2 things:
1. Make the handle part look forward.
2. Add an AimPart that should also look forward (Where the camera will be placed).

To make the handle part look forward, while keeping the mesh’s direction, we have to use 2 parts, one is an invisible part for the Handle and another is the gun mesh which will be welded to the handle.
We will be using this plugin to weld parts by selecting the first part then holding CTRL and selecting the second part then clicking on this button.

image

Make sure all parts of the gun are Massless and CanCollide off and only the Handle part is anchored!
For the AimPart you can create a new invisible part in the gun and name it AimPart then adjusting it’s position and then welding it, I made a plugin that can help you set the position of the AimPart by letting you place your camera where it is and preview it before testing.

Scripting the ADS

To script the ADS, we will need 2 things, RunService and SpringModule.
To install the spring module, create a ModuleScript in ReplicatedStorage, then copy & paste that script in it.

image

We can now require the module in our script like this

local SpringModule = require(game.ReplicatedStorage.SpringModule)

We also need RunService so you should add this at the start of the gun script.

local SpringModule = require(game.ReplicatedStorage.SpringModule)
local RunService = game:GetService("RunService")

After we installed the spring module, we can start scripting the ADS, we will first need to add an Offset value for the ViewModel which will be applied to the ViewModel after placing it at the camera.
In the ViewModel script add these lines:

local Offset = Instance.new("CFrameValue", script)
Offset.Name = "Offset"

And in the ViewModel’s RunService RenderStepped event change this

ViewModel:SetPrimaryPartCFrame(Camera.CFrame)

To

ViewModel:SetPrimaryPartCFrame(Camera.CFrame:ToWorldSpace(Offset.Value))

Try running the game and setting Offset.Value to CFrame.new(0, 0, 1) to see if it works. (You can use command bar at the buttom for that.)

We will need the player’s mouse to detect right mouse button events.

local Player = game.Players.LocalPlayer
local Mouse = Player:GetMouse()

And

local M2Held = false
Mouse.Button2Down:Connect(function()
	M2Held = true
end)
Mouse.Button2Up:Connect(function()
	M2Held = false
end)

Now to make working ADS, we need the ViewModel and the Offset from before

local Camera = workspace.CurrentCamera
local ViewModel = Camera:WaitForChild("ViewModel")
local Offset = Char:WaitForChild("ViewModel"):WaitForChild("Offset")

local Handle = script.Parent:WaitForChild("Handle")
local AimPart = script.Parent:WaitForChild("AimPart")

And then we can start working on the actual ADS in a RunService.RenderStepped event.

RunService.RenderStepped:Connect(function()
	-- ADS & Recoil code will be here.
end)

Since our ViewModel is a clone of the character not an arm model, we need to set the CFrame of that ViewModel and not the gun, so we will do some math to put the ViewModel in the correct position.

In the RenderStepped event add

local HandleTransform = ViewModel:GetPrimaryPartCFrame():ToObjectSpace(Handle.CFrame)
local OriginalTransform = HandleTransform * HandleTransform:Inverse()
local AimTransform = AimPart.CFrame:ToObjectSpace(Handle.CFrame) * HandleTransform:Inverse()

How this will work is:

  1. Get the transform of the Handle part to the ViewModel’s primary part (Which is used to set it’s position).
  2. Apply the negative transform to the original transform for the hipfire offset which will result in a neutral CFrame (This will be used later to apply recoil to original transformation).
  3. Apply the offset of the AimPart from Handle to the transform, so that AimPart is always positioned where the camera is.

Now we can see if it works by adding these lines after the previous lines (Also in the RenderStepped event).

if M2Held then
	Offset.Value = AimTransform
else
	Offset.Value = OriginalTransform
end

We should have this now

We can use Springs from the SpringModule to make this ADS smooth and realistic, we first need to create a spring before the RenderStepped event.

local AimSpring = SpringModule.new(0)
AimSpring.Damper = 1
AimSpring.Speed = 16

We will be using it to lerp between the OriginalTransform and AimTransform, so whenever the spring’s position is 0 it will be at original transform and whenever it’s at 1 it will be at aim transform (0.5 is halfway, etc).
To do this we will use CFrame:Lerp(), it works like this CFrame1:Lerp(CFrame2, Position)

Offset.Value = OriginalTransform:Lerp(AimTransform, AimSpring.Position)

Make sure you delete this part!

if M2Held then
	Offset.Value = AimTransform
else
	Offset.Value = OriginalTransform
end

Now we need to set the Spring’s target so it moves, at the start of the RenderStepped event add these lines.

if M2Held then
	AimSpring.Target = 1
else
	AimSpring.Target = 0
end

Spring.Position will always keep gradually changing towards Spring.Target so if we test this code we should have this.

We don’t want the gun to change the offset while it’s unequipped and interfere with other guns in inventory so add this line at the start of the RenderStepped event to end the function if the tool isn’t equipped.

if (script.Parent.Parent ~= Char) then
	return 
end

If you want to reset the transform the the hipfire position when the gun is unequipped add this instead.

if (script.Parent.Parent ~= Char) then
	AimSpring.Position = 0
	return
end

We should also reset the Offset when the tool is unequipped, add this in the script.Parent.Unequipped event.

Offset.Value = CFrame.new()

Adding Recoil

Now that we are finally done with the ADS, we can start with recoil.
We will create a new spring for the recoil.

local Recoil = SpringModule.new(Vector3.new())
Recoil.Speed = 16

Mouse.Button1Down:Connect(function()
	if (script.Parent.Parent ~= Char) then return end
	Recoil.Velocity = Vector3.new(0, 0, 12)
end)

This script should give a velocity to the recoil spring whenever the player left clicks with the tool equipped.
We need to apply the recoil to both transforms.

local HandleTransform = ViewModel:GetPrimaryPartCFrame():ToObjectSpace(Handle.CFrame)
local OriginalTransform = HandleTransform * CFrame.new(Recoil.Position) * HandleTransform:Inverse()
local AimTransform = AimPart.CFrame:ToObjectSpace(Handle.CFrame) * CFrame.new(Recoil.Position) * HandleTransform:Inverse()

This should be the result.


We can create another spring for camera recoil too, but it will be more advanced to adjust it’s rotation.

local Recoil = SpringModule.new(Vector3.new())
Recoil.Speed = 16

local CamRecoil = SpringModule.new(0)
CamRecoil.Speed = 15
CamRecoil.Damper = 0.8

Mouse.Button1Down:Connect(function()
	if (script.Parent.Parent ~= Char) then return end
	Recoil.Velocity = Vector3.new(0, 0, 12)
	CamRecoil.Velocity = 150 * 5
end)

To apply it we will need to substract the last applied rotation from the current rotation before applying it to camera, since we also want to camera to go a little higher after the recoil is over we will let 2% of the recoil be applied permanently.

local CamTransform = CFrame.fromEulerAnglesXYZ(math.rad(CamRecoil.Position), 0, 0)
Camera.CFrame = Camera.CFrame * (CamTransform * PreviousCamTransform:Lerp(CFrame.new(), 0.02):Inverse())
PreviousCamTransform = CamTransform

These lines should be inside the RenderStepped event.
Make sure to add this line before the event aswell.

local PreviousCamTransform = CFrame.new()

Here is the uncopylocked place incase you need it!

21 Likes

Really good tutorial, what would I do if I wanted to use a custom viewmodel?

If you want to use custom arms that means you would also use custom animations, you can have a model of that ViewModel in ReplicatedStorage and whenever the script loads it will clone that model instead of the player’s character, you would also have to make your script play animations on both the player’s character and the ViewModel because the automatic animation replication in the current script won’t work since they are different models, if you want to use R6 ViewModels on R15 players though (Like arsenal does) you can also have an arms model in ReplicatedStorage, but instead of setting the CFrame of the ViewModel in your script you set the CFrame of the gun and place the arms in their grip positions.

1 Like

Appreciate you making this awesome tutorial! Was just wondering when we can expect part 3?

Honestly I thought the tutorial has died for a while but since it’s coming back up I’ll probably start working on part 3.

Very cool, will look forward to part 3!

Is part 3 still in the works, when can we expect it?

1 Like

The tutorial is really nice! I really like how concise it was. It was really easy for me to understand it! Nice Job!

Sorry I’ve just been inactive on the devforum and after thinking about it, the tutorial is already not that good since the ViewModels don’t look quite good and have bad coding, plus Aim down sights will even make it worse.

At this point you should’ve understood the basics of a viewmodel and all I can suggest is looking for a more advanced tutorial, I suggest learning about springs, humanoids, animations, etc…

I don’t think I’ll ever make part 3.

You could also use the HumanoidDescription | Roblox Creator Documentation for body colors

Hi there, thanks for the tutorial. Just need a bit of help though, the actual characters arms won’t be transparent(not the viewmodels arms) when equipping the tool. It shows both the actual arms and the view models arms.

put a local script in startergui or smthn and add this code

local player = game.Players.LocalPlayer
local char = player.Character
local RunService = game:GetService("RunService")

char:WaitForChild("Humanoid").CameraOffset = Vector3.new(0, -.2, -1.1)

for i, v in pairs(char:GetChildren()) do
	if v:IsA("BasePart") and v.Name ~= "Head" and v.Name ~= "Left Arm" and v.Name ~= "Right Arm" then

		v:GetPropertyChangedSignal("LocalTransparencyModifier"):Connect(function()
			v.LocalTransparencyModifier = v.Transparency
		end)

		v.LocalTransparencyModifier = v.Transparency

	end
end

RunService.RenderStepped:Connect(function(step)
	local ray = Ray.new(char.Head.Position, ((char.Head.CFrame + char.Head.CFrame.LookVector * 2) - char.Head.Position).Position.Unit)
	local ignoreList = char:GetChildren()

	local hit, pos = game.Workspace:FindPartOnRayWithIgnoreList(ray, ignoreList)

	if hit then
		char:WaitForChild("Humanoid").CameraOffset = Vector3.new(0, 0, -(char.Head.Position - pos).magnitude)
	else
		char:WaitForChild("Humanoid").CameraOffset = Vector3.new(0, -.2, -1.1)
	end
end)

Thanks. I found out that it was a viewable arms local script that I had inputed earlier. Thank you though for the kind script. I although am facing another issue, where the tool with the ViewModel will be randomly removed. I can’t seem to find the problem, but shortly after equipping it will despawn.