So,
Its my first time making an FPS System and everything was working fine except once i got to equipping animation
the bug i encountered was that whenever the player equips a weapon it creates a new viewmodel and parents it to the camera
and then i play an equip animation
when i did that the viewmodel spawned in the default pose (no animation) and then the animation starts playing weirdly, my explanation of the situation is not that good
but i’ll show a video of my problem and the code
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Modules = ReplicatedStorage.Modules
local Assets = ReplicatedStorage.Assets
local Framework = Modules.Framework
local SharedFramework = Framework.Shared
local PackagesFramework = Framework.Packages
local Utility = require(SharedFramework.Utility)
local Spring = require(PackagesFramework.Spring)
local currentCamera = workspace.CurrentCamera
local recoilPositionSpring = Spring.new(Vector3.zero)
local recoilRotationSpring = Spring.new(Vector3.zero)
recoilPositionSpring.Speed = 10
recoilRotationSpring.Speed = 10
recoilPositionSpring.Damper = 0.8
recoilRotationSpring.Damper = 0.8
local bobTime = 0
local currentIntensity = 0
local player = game.Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local Viewmodels = {}
Viewmodels.Viewmodel = nil
Viewmodels.Connection = nil
Viewmodels.Animations = {}
Viewmodels.Sounds = {}
local function updateApperance(viewmodel:Model)
local RightArm:MeshPart = viewmodel.RightArm
local LeftArm:MeshPart = viewmodel.LeftArm
RightArm.Color = character:FindFirstChild("Body Colors").RightArmColor3
LeftArm.Color = character:FindFirstChild("Body Colors").LeftArmColor3
end
function Viewmodels:Equip(weapon : string)
-- UnEquip Old Viewmodel so we dont cause any Duplication Errors
if self.Viewmodel then self:UnEquip() end
-- Ensure the weapon is valid
if not weapon then return end
assert(typeof(weapon) == "string", string.format("Expected (%s) got (%s)", "string", typeof(weapon)))
-- Get the Viewmodel and Module from Utility Module
local weaponViewmodel = Utility.GetViewmodel(weapon)
if not weaponViewmodel then return end
-- Clone the viewmodel since it only returns the model from ReplicatedStorage without Cloning
local viewmodelClone = weaponViewmodel:Clone()
-- Assign the Viewmodel and Module so we can get data about the viewmodel or weapon for customizations
self.Viewmodel = viewmodelClone
self.Animator = viewmodelClone.Humanoid.Animator
self.Module = Utility.GetWeaponModule(weapon)
self.Viewmodel.Parent = currentCamera
updateApperance(self.Viewmodel)
-- PreLoad Animations and Sounds before parenting the viewmodel
-- to ensure there's no flickering or weird animations or other weird bugs
self:LoadAnimations()
self:LoadSounds()
-- play animation
local equipAnim: AnimationTrack = self:GetAnimation("Equip")
equipAnim:Play()
equipAnim.Priority = Enum.AnimationPriority.Action4
equipAnim:GetMarkerReachedSignal("End"):Connect(function()
local idleAnim = self:GetAnimation("Idle")
idleAnim:Play()
end)
-- Adjust Recoils
recoilPositionSpring.Speed = self.Module.Recoil.Speed
recoilRotationSpring.Speed = self.Module.Recoil.Speed
recoilPositionSpring.Damper = self.Module.Recoil.Damper
recoilRotationSpring.Damper = self.Module.Recoil.Damper
-- Connect to RenderStepped to update the viewmodel position and other data if needed
self.Connection = RunService.RenderStepped:Connect(function(dt)
self:Update(dt)
end)
print("[Viewmodels] Equipped:", weapon)
end
function Viewmodels:UnEquip()
local unequipAnim: AnimationTrack = self:GetAnimation("UnEquip")
unequipAnim:Play()
unequipAnim:GetMarkerReachedSignal("End"):Wait()
-- Disconnect RenderStepped Connection
if self.Connection then
self.Connection:Disconnect()
self.Connection = nil
end
-- Remove the Viewmodel
if self.Viewmodel then
self.Viewmodel:Destroy()
self.Viewmodel = nil
end
-- Clear Animations Table and Module
for _, anim in self.Animations do
anim:Stop()
end
-- Clear Sounds Table
for _, sound in self.Sounds do
if sound.IsPlaying then
task.spawn(function()
sound.Ended:Wait()
sound:Destroy()
end)
continue
end
sound:Destroy()
end
-- Cleanup
self.Sounds = {}
self.Animations = {}
self.Animator = nil
self.Module = nil
end
function Viewmodels:LoadSounds()
if not self.Viewmodel then return end
for name, id in self.Module.Sounds do
local sound = Instance.new("Sound")
sound.Name = name
sound.SoundId = id
sound.Parent = self.Viewmodel.PrimaryPart
-- Save Sound
self.Sounds[name] = sound
end
print("[Viewmodels] PreLoadded all Sounds:", self.Sounds)
end
function Viewmodels:LoadAnimations()
if not self.Viewmodel then return end
for name, id in self.Module.Animations do
local anim = Instance.new("Animation")
anim.Name = name
anim.AnimationId = id
anim.Parent = self.Viewmodel.PrimaryPart
self.Animations[name] = self.Animator:LoadAnimation(anim)
end
print("[Viewmodels] PreLoadded all Animations:", self.Animations)
end
function Viewmodels:GetAnimation(animation : string)
print('got animation:', animation)
-- Check if Animation is already Loaded, Then Return it
if self.Animations[animation] then
print("[Viewmodels] Got Cached", animation, "Animation:", self.Animations[animation])
return self.Animations[animation]
end
if not self.Viewmodel then return end
assert(typeof(animation) == "string", string.format("Expected (%s) got (%s)", "string", typeof(animation)))
-- Create Animation
local anim: Animation = Instance.new("Animation")
anim.AnimationId = self.Module.Animations[animation]
anim.Parent = self.Viewmodel.PrimaryPart
-- Load Animation and Save it in Animations Table so we can use it anytime later
local track: AnimationTrack = self.Animator:LoadAnimation(anim)
self.Animations[animation] = track
-- Cleanup Animation because we dont really need it
-- because the animation is loaded and we got the track now we can play animations
-- even if the animation itself got destroyed.
-- anim:Destroy()
print("[Viewmodels] Created New", animation , "Animation")
return track
end
function Viewmodels:PlaySound(soundName : string)
if not self.Sounds[soundName] then return end
assert(typeof(soundName) == "string", string.format("Expected (%s) got (%s)", "string", typeof(soundName)))
-- Play Sound
self.Sounds[soundName]:Play()
end
function Viewmodels:AddRecoil(position, rotation)
recoilPositionSpring.Impulse(recoilPositionSpring, position)
recoilRotationSpring.Impulse(recoilRotationSpring, rotation)
end
function Viewmodels:Update(dt)
if not self.Viewmodel then return end
local moving = Utility.IsPlayerMoving()
-- 1. Smooth Intensity Interpolation
-- Target is 1 if moving, 0 if standing still.
local targetIntensity = moving and 1 or 0
-- Lerp the intensity so the gun smoothly returns to center when stopping
currentIntensity = currentIntensity + (targetIntensity - currentIntensity) * math.min(dt * 10, 1)
-- 2. Continuous Time
-- Time always moves, but slightly faster when walking
local speedMultiplier = moving and 8 or 2
bobTime += dt * speedMultiplier
-- 3. Idle Breathing (Always active, ultra-minimal)
local breathX = math.sin(bobTime * 0.5) * 0.01
local breathY = math.cos(bobTime * 0.5) * 0.025
-- 4. Walk Bobbing (Scaled by currentIntensity)
-- Using sin(time) for X and cos(time * 2) for Y creates a perfectly smooth Figure-8
local walkX = math.sin(bobTime) * 0.035 * currentIntensity
local walkY = math.cos(bobTime * 2) * 0.035 * currentIntensity
local walkRoll = math.sin(bobTime) * math.rad(0.5) * currentIntensity
-- Combine the motions
local finalX = breathX + walkX
local finalY = breathY + walkY
local finalRoll = walkRoll
-- Recoil
local recoilPos = recoilPositionSpring.Position
local recoilRot = recoilRotationSpring.Position
-- Final CFrames
local bobCF = CFrame.new(finalX, finalY, 0) * CFrame.Angles(0, 0, finalRoll)
local recoilCF = CFrame.new(recoilPos) * CFrame.Angles(recoilRot.X, recoilRot.Y, recoilRot.Z)
self.Viewmodel:PivotTo(currentCamera.CFrame * bobCF * recoilCF )
end
return Viewmodels

