I’ve rewrote an improved version of this tutorial over here, please check it out instead of this one!
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
-
The first thing we should start with is creating our tool, setting up the main grip and welding gun parts together, for this tutorial we will use a free model for our mesh parts.
-
After inserting our model and deleting all scripts and unnecessary stuff, we should have something that looks like this:
And our tool should be configured like this:
-
Now we need to weld our tool parts and get it ready for animation, we need to make each part in the model have a special name & we will be using Moon Animator Suite to weld our parts (Can be used to animate too).
Now follow the steps in the video below to weld the parts of our tool.
You can do Ctrl+Mouse1 to select multiple parts, make sure theBase Part
is the handle and theTarget Part
is the other part
Now we repeat the same process for all the other parts, we should end up with this
Make sure all parts in the tool have Anchored and CanCollide properties off.
We can now edit the grip of our tool using tool grip editor plugin
If we put our tool inStarterPack
and start the game, we should have this:
Step 2: Coding the ViewModel & Creating Basic Animation
-
Now after we welded all gun parts, we need to animate them, but because welds cannot be animated, we have to create a separate model for animation, since animations that will be displayed globally won’t have moving parts such as magazine, bolt, etc
-
We can clone our tool and use Reclass to convert all Welds to Motor6Ds
-
After all parts are ready for animation, we can insert a dummy and insert our tool inside it (We will be using R6 in this tutorial, but it can work fine with R15)
-
We can now start animating using roblox’s animation plugin
Tip: You can set the framerate of the animation to 60 FPS by selecting it from the drop menu on the top right of the plugin’s window.
Make sure animation priority is set toAction
After finishing our animation, we can upload it to roblox and save the ID for later.
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)
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.
This is how it would work:
- Copy the player’s character while deleting it’s legs and putting it inside of the player’s camera.
- Setting the CollisionGroup of all parts of the ViewModel to a collision group that cannot collide with any other parts.
- 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
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:
- An
AimPart
is put into the gun, which will have a grip Vector3 set inLocalAnimations
module. - A regular grip Vector3 set in
LocalAnimations
where AimPart is placed into when the player is not aiming. - Smooth tweening between both grips during aiming.
- 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 settingAimPart
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.
- Yes
- No
0 voters